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 信息的方法称为反射。

猜你喜欢

转载自juejin.im/post/5d96a31af265da5b8f10718e