Java基礎知識掃盲(一)——編譯、基本類型、final、包裝器等

編譯和運行

  • javac 編譯文件 需要一個文件名(Welcome.java)
  • java 運行程序 需要指定類名(Welcome) 不需要帶擴展名
  • 通配符javac Employee*.java
  • EmployeeTest類中包含Employee類,直接編譯EmployeeTest類,Employee也會被自動編譯。可以認為Java編譯器內置了make/nmake功能

Microsoft_Program_Maintenance_Utility,外號NMAKE,顧名思義,是用來管理程序的工具。其實說白了,就是一個解釋程序。它處理一種叫做makefile的文件(以mak為后綴),解釋里面的語句并執行相應的指令。我們編寫makefile文件,按照規定的語法描述文件之間的依賴關系,以及與該依賴關系相關聯的一系列操作。然后在調用NMAKE時,它會檢查所有相關的文件,如果相關文件的time_stamp(文件最后一次被修改的時間,一個32位數)小于依賴文件(dependent_file)的times_tamp,NMAKE就執行依賴關系相關聯的操作。

打包文件

  • jar cvfm *.class將所有以.class結尾的字節碼文件打包到一個“JAR文件”

applet相關
appletviewer可以快速測試applet

  • Java區分大小寫
  • 訪問修飾符
  • 根據Java語言規范,main方法必須聲明為public
  • 每個Java應用程序都必須有一個main方法,并且是靜態的。
  • 每個句子必須用分號結束。特別需要說明,回車不是語句的結束標志, 如果需要可以將一條語句寫在多行上。
  • object.method(parameters)
  • 強語言類型,每個變量都需要聲明類型。

數據類型

8基本類型 4整型,2浮點型,字符類型char,和真值boolean型。

整型
byte 1字節,short 2字節,int 4字節,long 8字節

  • 長整型,后綴L或l(400000000000000L)
  • 十六進制,前綴0x或0X(0xCAFE)
  • 八進制,前綴0([010] = [8])
  • 二進制,前綴0b或0B([0b1001] = [9])
  • 數字面量,1_000_000(或0b1111_0100_0100_0000)表示一百萬。下劃線只是為了易讀。Java編譯器會去除這些下劃線。

浮點類型
float 4字節,double 8字節

  • float,后綴F或f(3.14F)
  • double,數值精度為float類型的兩倍。沒有后綴F的浮點數值(3.14),默認為double類型,也可以添加后綴D或d
  • 十六進制表示浮點數值。0.125 = 2的-3次方→0x1.0p-3。p表示指數,不是e。尾數采用十六進制,指數采用十進制。指數的基數是2,為不是10
  • Double.POSITIVE_INFINITY 表示正無窮大。
  • Double.NEGATIVE_INFINITY表示負無窮大。
  • Double.NaN表示不是一個數字。使用方法:
if(x == Double.NaN) //is never true
if(Double.isNaN(x)) // check whether x is "not a number"
  • 浮點數值不適用于無法接受誤差的金融計算中。
System.out.print(2.0 - 1.1);
0.8999999999999999
// 浮點數值采用二進制系統表示, 而在二進制系統中無法精確地表示分數 1/10
// 就好像十進制無法精確地表示出分數1/3一樣
// 不允許有任何舍入誤差,應該使用BigDecimal類

char類型

  • char類型可以表示為十六進制值。\u005B\u005D→[],注意注釋中的\u可能會產生語法錯誤。

boolean類型

  • false和true。整型值和布爾值不能轉換。

變量

  • 變量名的長度基本上沒有限制。
  • 哪些 Unicode 字符屬于 Java 中的“ 字母”可以存在變量中,使用Character類的isJavaldentifierStart、isJavaldentifierPart方法檢查。
  • 不能使用 Java 保留字作為變量名。
  • 可以在一行中聲明多個變量,但不推薦。
  • 不要使用未初始化的變量。

常量
const是Java保留字,但并未使用。必須使用final定義常量。

  • 用關鍵字final指示常量。表示這個變量只能被賦值一次。
  • 用關鍵字static final設置一個類常量。
  • 習慣上,常量名為全大寫。

運算符

  • 整數被0除,產生一個異常。
  • 浮點數被0除,得到無窮大或NaN結果。
  • 對于使用strictfp 關鍵字標記的方法必須使用嚴格的浮點計算來生成可再生的結果。如果將一個類標記為strictfp, 這個類中的所有方法都要使用嚴格的浮點計算。實際的計算方式取決于Intel處理器的行為
public static strictfp void main(String[] args)

數學函數
import static java.1ang.Math.*;

  • sqrt(x) 開方
  • pow(x,a) x的a次冪
  • floorMod(x,y) 對x求余y

自增與自減

int m = 7;
int n = 7;
int a = 2 * ++m; // now a is 16, m is 8
int b = 2 * n++; // now b is 14, n is 8

不建議在表達式中使用++

位運算符

  • & ("and")、 | ("or") 、^("xor") 、~ ("not")
    這些運算符按位模式處理。例如,n是一個整數變量,用二進制表示的n從右邊的第四位為1,則:
int m= (n & 0b1000)/0b1000;

返回1,否則返回0。利用&并結合使用適當的2的冪,可以把其他位掩掉只保留其中的某一位。

  • >>、<<運算符將位模式左移/右移
  • >>>運算符用0填充高位,>>會用符號位填充高位。不存在<<<

運算級別

  • a && b || c
    等價于
    (a && b) || c
  • a += b += c;(+=為右結合運算符)
    等價于
    a += (b += c);

字符串是否相等

  • s.equals(t)
  • "Hello".equals(greeting)
  • "Hello".equalsIgnoreCase("hello")忽略大小寫
  • 不要使用 == 檢測兩個字符串是否相等。只有字符串常量才是共享的,+或subString操作產生的結果是不共享的。
  • compareTo方法

空串和null

  • 空串“”,是一個對象:
    if(str.length == 0)
    if(str.equals(""))
  • null,沒有任何對象:
    if(str == null)

遍歷一個字符串,并且依次査看每一個碼點

int[] codePoints = str.codePoints().toArray();

把一個碼點數組轉換為一個字符串:

String str = new String(codePoints, 0, codePoints.length) ;

輸出格式
沿用了C庫函數的printf方法,可以進行格式化

double x = 333.333333333334f;
System.out.printf("%8.2f", x);
String name = "Jack";
int age = 12;
System.out.printf("Hello,%s,you'll be %d year-old", name, age);
printf轉換符.png
System.out.printf("%,.2f", 10000.0 / 3.0);
3,333.33
標志.png

switch語句

有可能觸發多個case分支,編譯代碼時可以考慮加上-Xlint:fallthrough選項,例如:

javac -Xlint:fallthrough Test.java

如果確實是想用這種“直通式(fallthrough)行為”,可以在外圍方法添加注解

@SuppressWarnings("fallthrough")

就不會對這個方法生成警告了


帶標簽的break

  • 跳出多重嵌套的循環語句。
read_data:
while(...){
...
  for(...){
    ....
    break read_data;
  }
}

標簽需要緊跟一個":"冒號。

  • 跳出if語句/塊語句
  • 只能跳出語句塊,不能跳入語句塊。

帶標簽的continue

跳至與標簽匹配的循環首部


大數值

BigInteger和BigDecimal
這兩個類可以處理包含任意長度數字序列的數值。BigInteger類實現了任意精度的整數運算,BigDecimal類實現了任意精度的浮點數運算。
常用Biglnteger API:

Biglnteger add(Biglnteger other) // +
Biglnteger subtract(Biglnteger other) // -
Biglnteger multipiy(Biginteger other) // * 
Biglnteger divide(Biglnteger other) // /
Biglnteger mod(Biglnteger other) // 取余

int compareTo(Biglnteger other) // 相等為0 小于為-1,大于為1
static Biglnteger valueOf(long x) // 返回一個值為x的Biglnteger 類型數據

常用BigDecimal API:

BigDecimal add(BigDecimal other) // +
BigDecimal subtract(BigDecimal other) // -
BigDecimal multipiy(BigDecimal other) // * 
BigDecimal divide(BigDecimal other, RoundingMode mode) // / 給出舍入方式,RoundingMode.HALF_UP 為四舍五入
Biglnteger mod(Biglnteger other) // 取余
int compareTo(Biglnteger other) // 相等為0 小于為-1,大于為1
static BigDecimal valueOf(long x); // 返回值為x
static BigDecimal valueOf(long x ,int scale);// 返回值為x/(10的scale次方)的實數

數組

聲明:int[] a=new int[100];
簡化: int[] b={1,2,3,4} → b = new int{1,2,3,4}

  • 整型數組初始化所有元素為0
  • boolean數組初始化所有元素為false
  • string數組初始化所有元素為null
  • new int[0] 與 null不同
  • Arrays.toString()方法返回一個包含數組元素的字符串

一旦創建了數組,就不能改變它的大小。如果需要經常在運行過程中擴展數組的大小,使用數組列表 array list有關數組列表

數組拷貝

int[] b = {1,2,3,4,5};
int[] a = b;
a[2] = 10; // b[2] == 10 為 true

上述代碼,兩個變量引用同一個數組。

int[] copied = Arrays.copyOf(b, b.length);

上述代碼,可以將一個數組的值拷貝到一個新數組中。第二個參數為新數組的長度。如果長度小于原數組的長度,則只拷貝前面的數據元素。

多維數組
即 數組的數組

多維數組.png

balances[i]表示引用第i個子數組,即第i行
balances[i][j]表示引用第i個子數組的第j項

double[][] balances = new double[10] [6]; // Java

等價于(分配了一個包含10個指針的數組)

double** balances = new double*[10] ; // C++
for (i = 0; i < 10; i++)
    balances[i] = new double[6];

多維數組的拷貝

int[][] a = {{1,2,3,4},{1,2,3,4},{1,2,3,4}};
int[][] b = a.clone();
System.out.println(a == b); // false

b[2][2] = 100;
System.out.println(a[2][2]);// 100

上述為淺拷貝,a,b為兩個不同的對象,但指向的是同一地址。因為是包含對象的對象。看下jdk描述:

clone官方解釋.png

尤其注意紅框部分:

clone中文解釋.png

即用clone()時,除了基礎數據和String類型的不受影響外,其他復雜類型(如集合、對象等)還是會受到影響的,除非你對每個對象里的復雜類型又進行了clone(),但是如果一個對象的層次非常深,那么clone()起來非常復雜,還有可能出現遺漏。

所以需要深克隆:實現Cloneable接口、重寫clone方法

值得一提的是,在Java中,任何對象變量的值都是對存儲在另外一個地方的一個對象的引用

Date d = harry.getHireDay();

返回可變數據域的引用.png

對d調用更改器方法同樣會修改這個雇員對象的私有狀態


日期

標準Java類庫包含了兩個類:

  • Date類 GWT格林威治時間,科學標準時間(遵循了世界上大多數地區使用的陽歷表示)
  • LocalDate類 日歷表示法(本地時間)
Date birthday = new Date();
Date deadline = birthday;
System.out.println(deadline); //  Wed Jul 11 11:22:22 CST 2018  // 
System.out.println(LocalDate.now()); // 2018-07-11

LocalDate date1999_12_31 = LocalDate.of(1999,12,31); // 1999-12-31
LocalDate localDate = date1999_12_31.plusDays(1000); // 返回一個新對象 不改變對象date1999_12_31
System.out.println(localDate);// 2002-09-26

GregorianCalendar someDay = new GregorianCalendar(1999,12,31);
someDay.add(Calendar.DAY_OF_MONTH, 1000);
System.out.println(someDay.get(Calendar.YEAR) + "-" + someDay.get(Calendar.MONTH) + "-" +
 someDay.get(Calendar.DAY_OF_MONTH));// someDay對象狀態會改變

只訪問對象而不修改對象的方法稱為訪問器方法(access method),更改器方法(mutator method)會改變對象狀態。

GregorianCalendar.add方法為更改器方法
LocalDate.getYear 和 GregorianCalendar.get 均為訪問器方法

打印當前月份日歷:

// 打印當月日歷
LocalDate date = LocalDate.now();
System.out.println("\n\nMON TUE WED THU FRI SAT SUN");
// 獲取月、日
int month = date.getMonthValue();
int day = date.getDayOfMonth();
// 獲取本月第一天
date = date.minusDays(day - 1);
// 本月第一天第一天為周幾?
int dayOfWeek = date.getDayOfWeek().getValue();
// 打印日歷第一行的縮進
for (int i = 1; i < dayOfWeek; i++) {
  System.out.print("    ");
}
// 打印主體
while (date.getMonthValue() == month) {
  System.out.printf("%3d", date.getDayOfMonth());
  if (date.getDayOfMonth() == day)
    System.out.print("*");
  else
    System.out.print(" ");
  date = date.plusDays(1);
  if (date.getDayOfWeek().getValue() == 1)
    System.out.println();
}

輸出如下:

MON TUE WED THU FRI SAT SUN
                          1 
  2   3   4   5   6   7   8 
  9  10  11* 12  13  14  15 
 16  17  18  19  20  21  22 
 23  24  25  26  27  28  29 
 30  31 

JavaSE8引入另外一些類來處理日期和時間

// 時間線 Instant 實現接口 Temporal
// 查看算法的運行時間
Instant start = Instant.now();
printCalendar();
Instant end = Instant.now();
Duration timeElapsed = Duration.between(start, end);
long millis = timeElapsed.toMillis();
System.out.println(millis); // 9

// 比較兩個算法運行時間
Instant start1 = Instant.now();
printCalendar1();
Instant end1 = Instant.now();
Duration timeElapsed1 = Duration.between(start1, end1);
boolean faster = timeElapsed1.minus(timeElapsed).isNegative();

Instant為時間線上的某個點
Duration.between獲取時間差。

日期調整器
TemporalAdjusters類提供了大量常見調整的靜態方法。

// 日期調整器
LocalDate nextTuesday = LocalDate.of(2018, 7, 1).with(TemporalAdjusters.nextOrSame(
  DayOfWeek.TUESDAY)); // 從2018.7.1號開始的下一個周二

// 實現自己的日期調整器
TemporalAdjuster NEXT_SUNDAY = w -> {
  LocalDate result = (LocalDate) w; //需要強制轉換  TemporalAdjuster任何一個實現類都可
  do {
    result = result.plusDays(1);
  }while (result.getDayOfWeek().getValue()!=7);
  return result;
};
System.out.println(LocalDate.now().with(NEXT_SUNDAY)); // 2018-7-15

//  TemporalAdjusters.ofDateAdjuster(UnaryOperator<LocalDate> dateBasedAdjuster) w默認為LocalDate類型則無須強制轉換
TemporalAdjuster NEXT_SATURDAY = TemporalAdjusters.ofDateAdjuster(w -> {
  LocalDate date = w;
  do {
    date = date.plusDays(1);
  } while (date.getDayOfWeek().getValue() != 6);
  return date;
});

與遺留util.Date類的轉換

Date birthday = new Date();
System.out.println(birthday);  // Wed Jul 11 15:47:36 CST 2018
birthday.toInstant(); // 2018-07-11T07:47:36.172Z
Date.from(Instant.now()); // Wed Jul 11 15:50:40 CST 2018

隱式參數與顯式參數

例如:

public void raiseSalary(double byPercent){
  double raise = salary * byPercent / 100;
  salary += raise;
}

其中byPercent為顯式參數,salary為隱式參數(方法的調用目標的參數),我們也可使用關鍵字this表示隱式參數,這樣便于將實例域與局部變量明顯區分開。

final實例域

對象包含final實例域,構建對象時則必須初始化這樣的域。即確保在每一個構造器執行之后。這個域的值被設置,并在后面的操作中,不能夠再對其修改。

final修飾符大多應用于基本(primitive)類型域,或不可變(immutable)類的域(如果類中的每個方法都不會改變其對象,這種類就是不可變的類)

對于可變對象,使用final容易造成混亂:

  private final StringBuiIder evaluations;

構造器構造:

  evaluations = new StringBuilder();

final的修飾只是表示存儲在evaluations變量中的對象引用不會指向其他StringBuilder對象。不過這個對象內容可以更改:

public void giveGoldStarO
{
evaluations.append(LocalDate.now() + ":Gold star!\n");
}

而String是個不可變的類。String中最核心的就是value[]值了,這個值不會被類中任何方法修改到。

private final char value[];

靜態域與靜態方法

將域定義為static,每個類中只有一個這樣的域。
爾每一個對象對于所有的實例域都有自己的一份拷貝。
靜態域可以理解為類域,屬于類所有。

class Employee{
    private static int nextId = 1;
    private int id;
}

每一個雇員都有一個自己的id域,但是Employee所有實例都共享一個nextId域。即,1000個Employee對象,有1000個實例域id,但是只有一個靜態域nextId。
即使沒有一個雇員對象,靜態域nextId也存在。它屬于類,不屬于任何獨立的對象。

public void setld()
{
id = nextld;
nextld++;
}

靜態變量使用較少,靜態常量使用較多。

public static final double PI = 3.14159265358979323846;

Math.PI就可以取到值。
如果沒有static這個字段,每個Math對象都將會有一份PI拷貝。

public final static PrintStream out = null;

System.out也是靜態常量。
每個類對象都可以對公有域進行修改,最好不要將域聲明為public。然而公有常量(final域)是沒問題的。因為被聲明為final,out不會被更改為其他打印流。

總結:類中聲明static域可以看作是公有量,static+final修飾的為公有常量。

/**
 * Reassigns the "standard" output stream.
 *
 * <p>First, if there is a security manager, its <code>checkPermission</code>
 * method is called with a <code>RuntimePermission("setIO")</code> permission
 *  to see if it's ok to reassign the "standard" output stream.
 *
 * @param out the new standard output stream
 *
 * @throws SecurityException
 *        if a security manager exists and its
 *        <code>checkPermission</code> method doesn't allow
 *        reassigning of the standard output stream.
 *
 * @see SecurityManager#checkPermission
 * @see java.lang.RuntimePermission
 *
 * @since   JDK1.1
 */
public static void setOut(PrintStream out) {
    checkIO();
    setOut0(out);
}

private static native void setOut0(PrintStream out);

setOut方法可以將System.out設置為不同的流,即修改final變量的值。
因為setOut0方法是native,本地方法不是用Java語言實現的,可以繞過Java的存取控制機制。

靜態方法
靜態方法是一種不能向對象實施操作的方法。可以認為靜態方法是沒有隱式的參數,沒有this參數的方法。

例如Emloyee類中靜態方法getNextld,

public static int getNextld()
{
return nextld; // returns static field
}

這里不能訪問實例域Id,因為靜態方法不可以操作對象。但是可以訪問類域。

建議用類名訪問靜態方法,不建議使用對象的引用調用靜態方法,防止混淆。

下面兩種情況才使用靜態方法

  • 方法只需要訪問類的靜態域(Emloyee.getNextld)
  • 方法不需要訪問對象狀態,所需參數都是通過顯式參數提供(eg: Math.pow)。

靜態方法main方法不對任何對象進行操作。啟動程序時,還沒有任何一個對象,靜態的main方法將執行并創建程序所需要的對象。

方法參數

1.對于基本類型參數:

public static void tripieValue(double x){ // 并不能修改x的值
    x = 3 * x;
 }

double percent = 10;
tripieValue(percent); // percent = 10;
對基本類型參數的修改沒有保存.png
  • x被初始化為percent的一個copy,10
  • x=x*3; 則x=30,但percent仍為10
  • 方法結束,參數變量x不再使用。

方法不可能修改一個基本數據類型的參數

2.對于對象參數:

public static void tripleSalary(Employee x){ // works
    x.raiseSalary(200) ;
}

harry = new Employee(. . .) ;
tripleSalary(harry) ; // harry.salary+200
對對象參數的修改保留了下來.png
  • x被初始化為harry的一個copy,這里是一個對象的引用。
  • x.raiseSalary(200),這個被引用的對象salary上漲200
  • 方法結束,參數變量x不再使用,對象變量harry繼續引用該對象。

3.Java對對象采用的就是引用調用嗎?答案是No
看個例子,swap為交換x,y

public void swap(Employee x, Employee y){
      Employee tmp = x;
      x = y;
      y = tmp;
 }
...
Employee a = new Employee("Alice", . . .);
Employee b = new Employee("Bob", . . .);
swap(a, b);
交換結果沒有保存下來.png
  • 方法結束時參數變量x和y被丟棄了。原來的變量a,b依然引用之前的對象。

Java 程序設計語言對對象采用的不是引用調用,實際上,對象引用是按值傳遞的

總結:Java中方法參數的使用情況:

  • 一個方法不能修改一個基本數據類型的參數(即數值型或布爾型)
  • 一個方法可以改變一個對象參數的狀態
  • 一個方法不能改變對象參數原有的引用(不能讓對象參數引用一個新的對象)

重載(overloading)

多個方法具有相同的名字、不同的參數即重載。簽名(方法名+參數類型)。

finalize方法

可以為任何一個類添加 finalize 方法,它將在垃圾回收器清楚對象之前調用。
但在實際應用中,不要依賴于使用finalize 方法回收任何短缺的資源,因為很難知道這個方法什么時候才能調用。

import java.util.;
import static java.lang.Math.
;//可以直接使用sqrt(pow(x, 2) + pow(y, 2));

文檔注釋

JDK有個很有用的工具: javadoc,可以由源文件生成一個HTML文檔

為以下幾部分編寫注釋:

  • 公有類與接口
  • 公有的和受保護的構造器及方法
  • 公有的和受保護的域

以/** ... */的格式,自由格式文本第一局應該是一個概要性的句子。javadoc自動將這些句子抽取出來形成概要頁。
<em>用于強調
<strong>著重強調
<img>圖片
不要使用<hl>或<hr> 會與文檔格式產生沖突。
{@code ...}等寬代碼
可以參考源碼中的標簽

類注釋
必須放在import語句之后,類定義之前。需要注意的是@see 分隔類名與方法名用的是#,多個see標簽要放在一起

/**
 * The {@code String} class represents   character strings. All
 * string literals in Java programs, such as {@code "abc"}, are
 * implemented as instances of this class.
 * @author  Lee Boynton
 * @see     java.lang.Object#toString()
 */

方法注釋
@param 變量描述
@return 描述
@throw 類描述

/**
 * Allocates a new {@code String} that contains characters from a subarray
 * of the character array argument. The {@code offset} argument is the
 * index of the first character of the subarray and the {@code count}
 * argument specifies the length of the subarray. The contents of the
 * subarray are copied; subsequent modification of the character array does
 * not affect the newly created string.
 *
 * @param  value
 *         Array that is the source of characters
 *
 * @param  offset
 *         The initial offset
 *
 * @param  count
 *         The length
 *
 * @throws  IndexOutOfBoundsException
 *          If the {@code offset} and {@code count} arguments index
 *          characters outside the bounds of the {@code value} array
 */
public String(char value[], int offset, int count)

域注釋
只需要對公有域(通常指的是靜態常量)建立文檔

/**
 * The {@code double} value that is closer than any other to
 * <i>pi</i>, the ratio of the circumference of a circle to its
 * diameter.
 */
public static final double PI = 3.14159265358979323846;

包與概述注釋
想要產生包注釋,需要在每一個包目錄中添加一個單獨的文件。兩種方式

  • 提供一個package.html命名的HTML文件。<body></body>中的所有文本都會被抽取出來
  • 提供一個以package-info.java命名的Java文件。這個文件必須包含一個初始的以 /**
    和 */ 界定的 Javadoc 注釋, 跟隨在一個包語句之后。它不應該包含更多的代碼或注釋。

為所有的源文件提供一個概述性的注釋。這個注釋將被放置在一個名為 overview.html 的文件中,這個文件位于包含所有源文件的父目錄中。標記 <body>... </body> 之間的所有文本將被抽取出來。當用戶從導航欄中選擇“Overview” 時,就會顯示出這些注釋內容。

注釋抽取

  • 切換到包含想要生成文檔的源文件目錄。 如果有嵌套的包要生成文檔, 例如 com.horstmann.corejava, 就必須切換到包含子目錄 com 的目錄(如果存在 overview.html 文件的
    話, 這也是它的所在目錄 )

  • 如果是一個包,應該運行命令:
    javadoc -d docDirectory nameOfPackage
    或對于多個包生成文檔, 運行:
    javadoc -d docDirectory nameOfPackage1 nameOfPackage2 . . .
    如果文件在默認包中, 就應該運行:
    javadoc -d docDirectory *. java

可參考javadoc相關文檔


繼承

例如Manager和Employee,Manager除了享有Employee待遇和屬性之外,還會有其他權利等。根本上講,Manager is a Employee,is-a 關系是繼承的一個明顯特征

  • Java中所有的繼承都是公有繼承。
  • 域繼承、方法繼承
  • 方法覆蓋、重寫
  • super可調用父類的方法、構造器。調用父類構造器的語句只能作為當前構造器的第一個語句出現。
  • 不支持多繼承
  • 在覆蓋一個方法的時候,子類方法不能低于超類方法的可見性

阻止繼承:final類和方法
不允許擴展的類被稱為final類,被final修飾的方法不能被子類覆蓋。

域也可以被聲明為 final。對于 final 域來說,構造對象之后就不允許改變它們的值了。 不過, 如果將一個類聲明為 final, 只有其中的方法自動地成為 final,而不包括域。

將方法或類聲明為 final 主要目的是: 確保它們不會在子類中改變語義。

多態

一個對象變量可以指示多種實際類型的現象被稱為多態,在運行時能夠自動選擇調用哪個方法的現象稱為動態綁定。如下e,即可引用Employee對象,又可引用Manager對象:

Manager boss - new Manager("Carl Cracker", 80000,1987, 12 , 15) ;
boss.setBonus(5000) ;

Employee[] staff = new Employee[3];
staff[0] = boss;
staff[1] = new Employee("Harry Hacker",  50000, 1989, 10, 1);
staff[2] = new Employee("Tony Tester", 40000, 1990, 3, 15);

for (Employee e : staff)
   System.out.println(e.getName0 + " " + e.getSalary());

對象變量是多態的,一個Employee變量既可以引用Employee對象也可引用Employee類的任何一個子類對象。

  • 子類數組的引用可以轉換成父類數組的引用,不需要強制轉換。
  • 將一個超類的引用賦給子類變量,必須進行類型轉換。并使用instanceof檢查。
  • 只能在繼承層次內進行類型轉換。

養成這樣一個良好的程序設計習慣: 在進行類型轉換之前, 先查看一下是否能夠成功地轉換。instanceof

if (staff[1] instanceof Manager){
  boss = (Manager) staff[1]:
}

在一般情況下,應該盡量少用類型轉換和 instanceof 運算符。并檢查一下超類的設計是否合理。

抽象類

  • 包含抽象方法的類必須被聲明為抽象的。
  • 抽象方法充當著占位的角色。可以提供公共的方法,但不需要寫具體實現。
  • 抽象類中也可以包含具體數據和具體方法。
  • 擴展抽象類:
  1. 子類中只定義部分抽象類方法或不定義抽象類方法,子類必須被標記為抽象類。
  2. 子類定義了全部的抽象方法,子類就不是抽象的了。
  • 抽象類不能被實例化
  • 可以定義一個抽象類的對象變量,但是它只能引用非抽象子類的對象

Object

equals方法
關于重寫equals,需要注意遵循自反、對稱、傳遞、一致性,equals(null)=false;

  • 如果子類能夠擁有自己的相等概念, 則對稱性需求將強制采用 getClass 進行檢測。
  • 如果由超類決定相等的概念,那么就可以使用 instanceof進行檢測, 這樣可以在不同子類的對象之間進行相等的比較,并且應將超類.equals設為final

完美equals建議:

  • 顯示參數名定為otherObject
  • 檢測 this 與 otherObject 是否引用同一個對象
  • 檢測 otherObject 是否為 null
  • 比較 this 與 otherObject 是否屬于同一個類,如果 equals 的語義在每個子類中有所改變,就使用 getClass 檢測,如果所有的子類都擁有統一的語義,就使用 instanceof 檢測。
  • 將otherObject轉為相應的類型。
  • 對所有需要比較的域進行比較了。使用=比較基本類型域,使用 equals 比較對象域。如果所有的域都匹配, 就返回 true; 否則返回 false。
  • 如果在子類中重新定義 equals, 就要在其中包含調用 super.equals(other)。
public boolean equals(Object otherObject){
  if (this = otherObject) return true;
  if (otherObject = null) return false;
  if (getClass() != otherObject.getClass()) return false; // equals 的語義在每個子類中有所改變
  if (!(otherObject instanceof ClassName)) return false; //所有的子類都擁有統一的語義 
  ClassName other = (ClassName) otherObject;
  return field1 == other.field1 && Objects.equa1s(field2, other.field2) && ... ;
}

String中的equals

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

hashCode方法
如果重新定義 equals方法, 就必須重新定義hashCode 方法,以便用戶可以將對象插人到散列表中
String中的hashCode()

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;

        ————for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}
  • hashCode 方法應該返回一個整型數值(也可以是負數
  • 合理地組合實例域的散列碼,以便能夠讓各個不同的對象產生的散列碼更加均勻

比較好的做好是組合多個散列值,調用 Objects.hash 并提供多個參數:

public int hashCode(){
  return Objects,hash(name, salary, hireDay);
}

如果存在數組類型的域, 那么可以使用靜態的 Arrays.hashCode 方法計算一個散列碼,這個散列碼由數組元素的散列碼組成

toString方法

強烈建議為自定義的每一個類增加 toString 方法。

Employee:

public boolean equals(Object otherObject){
  // a quick test to see if the objects are identical
  if (this == otherObject) return true;
  // must return false if the explicit parameter is null
  if (otherObject == null) return false;
  // if the classes don't match, they can 't be equal
  if (getClass() != otherObject.getClass() ) return false;
  // now we know otherObject is a non-null Employee
  Employee other = (Employee) otherObject;
  // test whether the fields have identical values
  return Objects.equals(name, other.name) && salary == other.salary&& Objects.equals(hireDay, other.hireDay) ;

public String toString(){
    return getClass().getName() + "[name:" + name +",salary:" + salary + ",hireDay=" + hireDay + "]";
}

Manager:

public boolean equals(Object otherObject){
  if (!super.equals(otherObject)) return false;
  Manager other = (Manager) otherObject;
  // super.equals checked that this and other belong to the same class
  return bonus == other.bonus;
}

public String toString(){
  return super.toString() + "[bonus=" + bonus + "]";
}

泛型數組列表

ArrayList<Employee> staff = new ArrayList<Employee>();

如果調用 add 且內部數組已經滿了,數組列表就將自動地創建一個更大的數組,并將所有的對象從較小的數組中拷貝到較大的數組中。

如果已經清楚或能夠估計出數組可能存儲的元素數量, 就可以在填充數組之前調用
ensureCapacity方法:

staff.ensureCapacity(100);
ArrayList<Employee> staff = new ArrayList<>(100) ;

注意區分 數組列表和數組 分配空間的區別:

  • new ArrayList<>(100) // capacity is 100
    數組列表只是擁有保存100個元素的潛力(實際上,重新分配空間的話,會超過100),但是最初(初始化構造),數組列表不含有任何元素
  • new Employee[100]// size is 100
    數組有100個位置可用

確認數組列表的大小不再發生變化,可以調用trimToSize方法。這個方法將存儲區域的大小調整為當前元素數量所需要的存儲空間數目。垃圾回收器將回收多余的存儲空間。

list.add(0,"hello"); //添加新元素
list.set(0,"world"); //替換已有元素內容
list.add(n,e); // 位于n之后所有元素向后移動一個位置
list.remove(n); // 移除n下標元素,且之后的元素都前移一個位置

轉數組:

ArrayList<X> list = new ArrayList();
while (. . .){
  x = . .
  list.add(x);
}

X[] a = new X[list.size()];
list.toArray(a);
  • 為了能夠看到警告性錯誤的文字信息,要將編譯選項置為 -Xlint:unchecked
  • 能確保不會造成嚴重的后果, 可以用@SuppressWamings("unchecked") 標注來標記這個變量能夠接受類型轉換

對象包裝器和自動裝箱

包裝器(wrapper):Integer類對應基本類型int
Integer、Long、Float、Double、Short、Byte(前6個類派生于公共的超類Number)、Character 、 Void 和 Boolean

  • 對象包裝器類是不可變的,即一旦構造了包裝器,就不允許更改包裝在其中的值。
  • 對象包裝器類還是 final , 因此不能定義它們的子類。
public final class Integer extends Number implements Comparable<Integer> 

自動裝箱(autoboxing)

ArrayList<Integer> list = new ArrayList<>();
list.add(3);
// 自動變成
list.add(Integer.value0f(3));

list.add(3)的調用即為自動裝箱

自動拆箱

int n = list.get(i);

即編譯器會翻譯為int n = list.get(i).intValue();

Integer n = 3;
n++;

編譯器自動插入一條對象拆箱指令,然后自增,然后再將結果裝箱

Integer a = 1000;
Integer b = 1000;
System.out.println(a == b); // false

Integer c = 100;
Integer d = 100;
System.out.println(c == d); // true

== 檢測的是對象是否指向同一個存儲區域。自動裝箱將經常出現的值包裝到同一個對象中,即:
自動裝箱規范要求 boolean、byte、char 127,介于-128 ~ 127之間的 short 和 int 被包裝到固定的對象中。

所以經過自動裝箱,a,b所指向的100是同一個對象,而c,d所指向的1000是兩個不同的對象。

為了避免這種不確定,兩個包裝器對象比較時,調用equals方法

在一個表達式中混用Integer和Double,Integer會拆箱,提升為double,再裝箱為Double

Integer n = 1;
Double x = 2.0;
System.out.println(true ? n : x); // 1.0

敲黑板~ 裝箱和拆箱是編譯器認可的, 而不是虛擬機。編譯器在生成類的字節碼時, 會插入必要的方法調用。虛擬機只是執行這些字節碼。

有些人認為包裝器類可以用來實現修改數值參數的方法, 然而這是錯誤的。

public static void triple(int x) {// won't work
  x = 3 * x; // modifies local variable
}

public static void triple(Integer x) // won't work

因為Java是值傳遞:int基本類型參數不會變、Integer對象不可變:包含在包裝器中的內容不會變。

如果需要一個修改數值參數值的方法,需要使用在 org.omg.CORBA包中定義的持有者(holder)類型,包含IntHolder、BooleanHolder等。每個持有者類型都包含一個共有域值,通過它可以訪問存儲在其中的值。

public final class IntHolder implements Streamable {

/**
 * The <code>int</code> value held by this <code>IntHolder</code>
 * object in its <code>value</code> field.
 */
public int value;

如下:

{
...
IntHolder y = new IntHolder();
y.value = 3;
changeInt(y);
System.out.println(y.value); // 9
...
public static void changeInt(IntHolder x){
  x.value = x.value * 3;
}}

參數數量可變的方法

eg printf:

public PrintStream printf(String format, Object ... args) {
    return format(format, args);
}

這里的 ... 省略號 是Java代碼的一部分,表明這個位置可以接收任意數量的對象。
即fmt的第i個格式說明符與arg[i]的值匹配

public static double max(double ... values){
  double largest = Double.NEGATIVE_INFINITY;
  for (double val : values)
    if (val > largest) largest = val;
  return largest;
}
...
// 調用
double[] values = {1.2, 123123.12, -0.999, 12312};
System.out.println(max(values)); // 123123.12

System.out.println(max(1.2, 123123.12, -0.999, 12312)); // 123123.12

枚舉類

public enum Size {
  SMALL("S"), MEDIUM("M"), LARGR("L"), EXTRA_LARGE("XL");
  
  private String abbr; // 縮寫
  
  private Size(String abbr) {
    this.abbr = abbr;
  }
  
  public String getAbbr() {
    return abbr;
  }
  
  public static void main(String[] args) {
    SMALL.getAbbr(); // S
    SMALL.toString(); // SMALL

    Size s = Enum.valueOf(Size.class, "SMALL");// toString的逆方法 valueOf

    Size[] sizes = Size.values();
    System.out.println(sizes);

    Size.EXTRA_LARGE.ordinal(); // 聲明中枚舉常量的位置 3
  }
}
sizes數組.png

反射(reflective)5.7

能夠分析類能力的程序稱為反射。反射機制可以用來:

  • 在運行時分析類的能力。
  • 在運行時查看對象
  • 實現通用的數組操作代碼
  • 利用Method對象(類似于C++中的函數指針)

繼承的設計技巧

  • 將公共操作和域放在超類
  • 不要使用受保護的域
  • 使用繼承實現“ is-a” 關系
  • 除非所有繼承的方法都有意義, 否則不要使用繼承
  • 在覆蓋方法時,不要改變預期的行為
  • 使用多態,而非類型信息
  • 不要過多地使用反射
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容