1. 程式人生 > 程式設計 >Java 雜記(一):Java Core

Java 雜記(一):Java Core

基本型別

Boxing

Java 支援自動裝箱,但是用過 C# 的人就會明白它和程式設計師真正理想的還差很遠(做到了無裝箱類),它只會在賦值時呼叫valueOf。比如說,我們有一個IntStream,而我們想轉成一個int[],此時呼叫toArray並不可以直接賦值,而要使用boxed。當然,這種不完美也和泛型有關,比如我們不能宣告一個基本型別的泛型集合(卻可以宣告一個基本型別的陣列)。

Cache Pools

Java 的常量池具有快取的特性。

Cache to support the object identity semantics of autoboxing for values between -128 and 127 (inclusive) as required by JLS.

通過Integer的原始碼可以看到,緩衝池所能快取的數值有上下界,同時上界可以通過配置修改:

-XX:AutoBoxCacheMax=number
-Djava.lang.Integer.IntegerCache.high=number
複製程式碼

物件

Access modifiers

Scala對Java的訪問控制做了一些改進。

第一,Java允許外部類訪問內部類的私有成員。這一點有點讓人奇怪。

第二點是,Scala重定義了protected。這曾經也是困擾我的一大問題:因為我總覺得這就是應該如此:protected不應該包含包內可見這個語義。

當然,Scala實現了更為複雜的組合語義,來保證原先的包內可見仍然是有效的。

Single method interface

Java 的介面預設方法是一個非常優異的特性,這個在 C# 最新版本才被實現。

介面預設方法有一個重要的用途是介面演化(interface evolution)。換句話說,使用介面預設方法不會對已經實現的類造成影響。

如何解決多繼承問題導致的介面預設方法衝突?

  • 超類優先
  • 介面間衝突

換句話說,介面是沒有順序的。超類優先,或者說類優先可以保證向後相容。

介面的欄位預設都是 static 和 final 的。

Object 通用方法

public native int hashCode()

public boolean equals(Object obj)
protected native Object clone() throws CloneNotSupportedException public String toString() public final native Class<?> getClass() protected void finalize() throws Throwable
{} public final native void notify() public final native void notifyAll() public final native void wait(long timeout) throws InterruptedException public final void wait(long timeout,int nanos) throws InterruptedException public final void wait() throws InterruptedException 複製程式碼

equals & hashCode

Java相等性的設計是有問題的,就String的比較已經可以看到。這一點許多大牛都提過,比如 Martin 老爺子。但這反而構成了對程式設計師能力的考察,不得不說也是一種諷刺。C#和Scala都提供了直接對引用進行比較的方法,並且都支援運運算元過載(雖然實現機制就不同了)。

equals的實現是Java和C#最重要的模版程式碼之一。這其實和equals本身的設計有關:由於放在了基類Object之中,只能將引數作為基類傳入。

  • 檢查是否為同一個物件的引用,如果是直接返回 true;
  • 檢查是否是同一個型別,如果不是,直接返回 false;
  • 將 Object 物件進行強制型別轉換;
  • 判斷每個關鍵域是否相等。

equals必須滿足閉包的3個條件。另外,還需要具有冪等性,以及將null歸入到閉包中。建議看下Programming in Scala關於相等性一節的推導。

重寫equals意味著必須同步hashCode方法。關於如何對所有的域計算合適的雜湊,Java的字串已經給出了範例。

toString

話說希望以後能給Typora提個Issue,讓貼上的程式碼自動去掉空格就好了。

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
複製程式碼

clone

如果一個類沒有實現Cloneable卻重寫了clone方法,就會丟擲CloneNotSupportedException。這一條軍規很有趣。

另外,clone本身是protected的,所以每次我們過載的時候都需要修改訪問修飾符為public

預設object中的是淺拷貝(Shallow Clone)。最好不要去使用clone,可以使用拷貝建構函式或者拷貝工廠來拷貝一個物件——這是《Effective Java》裡建議的。

Property

Java是沒有屬性的,這一點很讓人遺憾。Lombok作為一個庫補足了這一點,但代價是必須在依賴和IDE裡配置——也許有時候反而得不償失。

字串

Java 的字串有許多設計點。

首先就是不可變性——String類被final修飾。實際上基本型別的裝箱類都是final的。不僅如此,他們的field也是final。這個好處就不多說了,比如不可變類可以用作雜湊的鍵值。不可變性代表著我們必須計算雜湊來保證唯一性。字串的雜湊很有代表性:首先它實現了快取;其次它使用了一個31進位制的多項式加法,並用Horner's method加速運算。31是一個奇素數,學過離散數學的都知道這對於冪具有很好的分散性。你也可以看作是,這個乘積如果包含了2,那麼就相當於最終進行了左移。

在 Java8之前, Java 採用了一種共享字元陣列的手法來生成 substring。這樣做無可厚非(如果是我也會直觀的這樣想),但是卻會導致記憶體洩露。因此之後 Java 就不再採用這種做法了:

public String(String original) {
    this.value = original.value;
    this.hash = original.hash;
}
複製程式碼

String對null的拼接是當作一個字串"null"來處理,並且寫在實現裡的。這一點Scala也是如此。但C#做的更優雅。

In string concatenation operations,the C# compiler treats a null string the same as an empty string,but it does not convert the value of the original null string.

new String("abc")

Date

Java 儲存時間的方式是距離 epoch(1970-01-01 00:00:00 UTC)的時間間隔,也就是 timestamp。

LocalDateDate區別在於,Date是UTC時間,而LocalDate.now()內部呼叫的是Clock.systemDefaultZone()方法(也可以通過傳遞時區引數呼叫),即時區相關的,另外它返回的是ISO-8601(yyyy-MM-dd)格式,是無時間的。

What's the difference between Instant and LocalDateTime?

新的日期 API 裡的物件都是不變類。

Java 8中引入的日期API是JSR-310規範的實現,Joda-Time框架的作者正是JSR-310的規範的倡導者,所以能從Java 8的日期API中看到很多Joda-Time的特性。

隱式型別轉換

Java 有一點會讓 C# 程式設計師非常奇怪:

short a = 0;
int b = 1;
a += b;
System.out.println(a);
複製程式碼

使用2階段賦值(Compound assignment )運運算元,比如++或者+=,會進行隱式的型別轉換。我更喜歡把它叫做,隱式的強制型別轉換。

StackOverflow : Why don't Java's +=,-=,*=,/= compound assignment operators require casting?

final

在 C# 裡有兩個關鍵字來表示這一個:readonlysealed。這個設計不能說優劣,只能說 Java 有時候居然出奇的……靈活?

static

這個關鍵字有一些比較特殊的用法。

  • 靜態方法必須有實現,不能是抽象方法。
  • 靜態語句塊在類初始化時優先執行。靜態程式碼塊優先順序最高。
  • 非靜態內部類依賴於外部類的例項,而靜態內部類不需要。

注意到,不論是靜態方法還是例項方法,都只有一個副本存在於Class檔案中。這是一個很自然的設計。

存在繼承的情況下,初始化順序為:

  • 父類(靜態變數、靜態語句塊)
  • 子類(靜態變數、靜態語句塊)
  • 父類(例項變數、普通語句塊)
  • 父類(建構函式)
  • 子類(例項變數、普通語句塊)
  • 子類(建構函式)

static nested class

Nested classes are divided into two categories: static and non-static. Nested classes that are declared static are called *static nested classes*. Non-static nested classes are called *inner classes*.

Switch

C# (在比較新的版本中)和 Scala 都實現了 switch 的模式匹配。Java 雖然支援字串的 switch(這是很自然的,因為前面也說過字串有快取),卻不支援 long 這樣的基本型別。

PS:Java 13 目前也支援模式匹配了。

異常

Java Checked Exception的機制一直飽受詬病,它會造成一種擴散的程式碼風格。

如何評價王垠的《Kotlin和Checked Exception》?

所以推薦自定義異常繼承RuntimeException這樣的非受檢異常。

有一種特殊的情況是,如果我們在finallytry都丟擲異常,那麼前者可以覆蓋掉後面的異常,這被叫做抑制異常(Suppressed Exceptions)。可以通過addSupressed來將被抑制的異常作為輔助資訊一起輸出(但前提是我們使用特定的變數捕獲它)。

還有一種方法是使用Java 7引入的try-with-resource語句(其實就是C#的using),也就是資源的自動釋放。它可以保證try可以反過來抑制資源釋放時的異常。

註解

Java在1.5以後引入了註解,但是和C#最大的區別是,註解是一個介面而非C#的類。這個設計決策很有意思。但是,它允許儲存值,這就是預設用value的原因。可以通過介面的方法定義來宣告註解的成員,並且通過特殊的default關鍵字賦值。

不過,顯然這裡是有一個問題的:介面能夠有資料嗎?答案就是Java用了動態代理類。這也是Java玩膩了的手段了。

java註解是怎麼實現的?

我按照知乎上大佬的教學,用hsdb除錯了一下。

java -classpath $JAVA_HOME/lib/sa-jdi.jar sun.jvm.hotspot.HSDB
複製程式碼

不過用起來還是沒有在IDEA裡爽,太麻煩了,唯一的好處是可以獲取Memory View沒有的資訊,但是這一點可以通過IDEA的VisualVM外掛補足,或者直接用java自帶的jstat粗略檢視。

可以看到動態代理類中包含AnnotationInvocationHandler例項,並且AnnotationInvocationHandlertypememberValues都會被填成這個Annotation對應的值。其實還有一個成員是memberMethods,但一般都是空的。

既然不論如何Java最終還是生成了一個類,為什麼不在一開始就把註解設計為類呢?

泛型

Java 泛型的設計真的是很差,雖然是為了相容不得已而為之:

In the generics design,there were a lot of very,very hard constraints. The strongest constraint,the most difficult to cope with,was that it had to be fully backwards compatible with ungenerified Java. The story was the collections library had just shipped with 1.2,and Sun was not prepared to ship a completely new collections library just because generics came about. So instead it had to just work completely transparently.

Martin Odersky

Java 泛型中臭名昭著的型別擦除理念,都是為了位元組碼的向後相容。JVM 的位元組碼中完全消去了型別資訊,這樣一來就不用修改 JVM 了。結果是,這個問題變成了讓無數 Java 開發者麻煩的根源——然而如果他們沒使用過其他語言,甚至不知道自己被坑了(從某種意義上,就像我這樣沒出過國的人一樣……)。

比如,Java 不能使用基本型別的集合,因為泛型的 T 必須是物件——它在泛型擦除之後就是 Object;我們也不能 new 一個泛型,因為根本沒有這個型別資訊;泛型不能用於靜態變數,因為 Java 的泛型本質上是一個型別,而 C# 這樣的是多個型別,所以 Java 的靜態變數都共享一個靜態例項,因此存在型別安全問題。靜態方法我猜測是考慮到對靜態變數的使用,因此也不允許。

雖然 Java 具有泛型擦除,我們卻可以通過getGenericSuperclass獲得父類的泛型引數。這一點非常有趣,因為這說明繼承可以持有泛型引數。

有一點有意思的地方是,Java 的泛型方法是把型別引數放在方法名的前面,而 C# 和 class 一樣放在後面。

Java中的泛型預設是不變的,而 C# 允許協變和抗變。這一點非常重要,尤其是對於集合和委託(這個特性 Java 本身沒有,但是也會有泛型介面來代替)。

要注意萬用字元<?>是一個具體型別。

反射

每個類(和介面)都有一個 Class 物件,包含了與類有關的資訊。當編譯一個新類時,會產生一個同名的.class 檔案。類在第一次使用時才動態載入到 JVM 中。也可以使用 Class.forName("com.mysql.jdbc.Driver") 這種方式來控制類的載入,該方法會返回一個 Class 物件。

通過Class例項獲取 class 資訊的方法稱為反射。