Java类型系统

κiζsヤ當非主流成為主流シ
java一直是面向对象语言的代表。目前大部分语言都支持面向对象,同时面向对象的概念也变得模糊。

Java的面向对象主要有两点:

  1. 数据和函数打包,在这里函数称为方法,他的第一个参数默认是this。
  2. 基于继承和子类型的类型系统。

这两点又有一点交叉,this指向的对象类型不同,会分派到不同的方法(虚方法)。而对于第一点,他又和闭包类似,Java8以前用对象来模拟函数,事实上展示了两者的共性。而这种设计暗示对象是有状态的,这种设计和函数式不同。在函数式编程中,可变状态应该从参数中传入,从返回值中取出。

而第二点,一直被各种黑的假范型,以及为了兼容裸类型做出的妥协(通配符),也是这个语言最独特(恶心)的地方。

一个反直觉的例子

1
2
3
4
5
6
7
static <E> void test(List<E> l1,List<E> l2){

}
public static void main(String[] args) {
List<?> list = new ArrayList<>();
test(list, list);
}
1
2
3
4
5
6
7
8
9
10
11
12
Main.java:12: error: method test in class Main cannot be applied to given types;
test(list, list);
^
required: List<E>,List<E>
found: List<CAP#1>,List<CAP#2>
reason: inference variable E has incompatible equality constraints CAP#2,CAP#1
where E is a type-variable:
E extends Object declared in method <E>test(List<E>,List<E>)
where CAP#1,CAP#2 are fresh type-variables:
CAP#1 extends Object from capture of ?
CAP#2 extends Object from capture of ?
1 error

错误提示了这两个List<E>不是一个东西,在不同的地方代表着不同的类型。假如我们想暗示两个参数是同一个类型,像这样:

1
2
3
4
5
6
7
static <E,L extends List<E>> void test(L l1,L l2){

}
public static void main(String[] args) {
List<?> list = new ArrayList<>();
test(list, list);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
Main.java:12: error: method test in class Main cannot be applied to given types;
test(list, list);
^
required: L,L
found: List<CAP#1>,List<CAP#2>
reason: inference variable E has incompatible equality constraints CAP#1,CAP#2
where L,E are type-variables:
L extends List<E> declared in method <E,L>test(L,L)
E extends Object declared in method <E,L>test(L,L)
where CAP#1,CAP#2 are fresh type-variables:
CAP#1 extends Object from capture of ?
CAP#2 extends Object from capture of ?
1 error

依然不能通过编译,问题在于List的元素类型对不上。于是我们加上? extends E,l1和l2的类型参数依然不同,但这一次E代表这两个类型的共同上界。

1
2
3
4
5
6
7
static <E,L extends List<? extends E>> void test(L l1,L l2){

}
public static void main(String[] args) {
List<?> list = new ArrayList<>();
test(list, list);
}

存在类型

此处不深入讨论类型论,而且我也不懂,仅展示存在类型和范型之间的区别。以Rust为例,这个语言就不支持子类型,同时Rust中的闭包又如此特别。比如:

1
2
3
4
5
6
fn main() {
let i = 0;
let j = 1;
let mut f = || i;
f = || j;
}
1
2
3
= note: expected closure `[closure@src/main.rs:4:17: 4:21]`
found closure `[closure@src/main.rs:5:9: 5:13]`
= note: no two closures, even if identical, have the same type

世界上没有两片相同的叶子,Rust里也没有两个相同的closuure

而一个独一无二的东西,在不支持子类型的同时,要如何表示他的类型呢?答案就是存在类型。

1
2
3
4
fn closure() -> impl Fn() -> i32{
let i = 1;
move || i
}

返回值在函数内部给出,此时编译器已经不能通过类型推导来帮我们做决定了,这也是存在类型在Rust中的用处。

回到java

java的范型也有一些类似,他是不变的(相对于协变和逆变),比如常见的例子:

1
2
3
4
5
6
interface Fruit {}
static class Apple implements Fruit{}

public static void main(String[] args) {
LinkedList<Fruit> list = new LinkedList<Apple>();
}
1
java: incompatible types: java.util.LinkedList<com.company.Main.Apple> cannot be converted to java.util.LinkedList<com.company.Main.Fruit>

这个例子是一个协变的逻辑,而List<Fruit>的在类型参数Fruit处是不变的。java的协变与其他语言(c#,scala)区别在于java的协变逻辑在使用的时候给出,而其他语言在定义的时候给出。或者说他可能和协变本身就不一样,java语言规范中关于协变唯二提到的部分是虚方法中的返回值协变,以及协变返回值方法和方法覆盖之间的关系。

而这一点也同样指出了返回值和参数类型推导的不同。

1
2
3
List<String> test(){
return (List<? extends String>)null;
}

显然这份代码也无法通过编译,因为返回值协变的前提是返回值类型必须是方法标出的返回值类型的子类型。

如何理解

存在类型如此难以理解和反直觉,在Rust语言中也同样是一个槛。和其他的语法限制一样,他包含两部分:

  1. 如何理解语法本身
  2. 在限制之下如何去写代码

我认为第二部分更加重要,比如Rust的lifetime和ownership本身并不难理解,但是形成一套什么样的代码是合法的直觉相当的困难。

从这一点上来说,在什么时候用协变逆变可以参考scala的说法:

These positions are classied as covariant for the types of immutable fields and method results, and contravariant for method argument types and upper type parameter bounds. Type arguments to a non-variant type parameter are always in non-variant position. The position flips between contra- and co-variant inside a type argument that corresponds to a contravariant parameter. The type system enforces that covariant (respectively, contravariant) type parameters are only used in covariant (contravariant) positions.

简要来说代表不可变的字段和返回值的类型参数是协变的,而代表方法的参数和类型参数上界的类型参数是逆变的,在逆变参数的内部协/逆变又是反过来的。他们同时出现的时候就会特别阴间:

1
2
3
public static <T> void sort(List<? extends Comparable<? super T>> list) {
list.sort(null);
}

这是标准库中的代码,List的元素类型是协变的,Comparable的类型参数是compareTo的参数的类型是逆变的。

协变相对于逆变更容易理解,一个集合类型存储着不同的元素,这些元素有公共父类型。但协变的前提是集合不可变,这也是为什么java的协变数组是有问题的。

1
2
3
4
5
6
7
8
interface Fruit {}
static class Apple implements Fruit{}
static class Orange implements Fruit{}

public static void main(String[] args) {
Fruit[] fruit = new Apple[1];
fruit[0] = new Orange();
}
1
2
Exception in thread "main" java.lang.ArrayStoreException: com.company.Main$Orange
at com.company.Main.main(Main.java:17)

而理解逆变的关键在于应该把函数作为一个整体,而不是单独去看类型参数为什么应该是下界(super)。可变性本身描述的就是假如A是B的子类型,两个值之间的子类型关系。而对于函数来说,一个函数类型A对于另一个函数类型B适用面更广,那么A类型的值可以出现在要求出现B类型的地方。

以水果为例,假设水果之间可以比较重量。如果不标出下界,父类已经实现了Comparable接口,但是不能编译通过,这无形中限制了代码的表达能力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Fruit extends Comparable<Fruit> {
@Override
default int compareTo(Fruit o){
return 0;
}
}
static class Apple implements Fruit{}
static <T extends Comparable<T>> void sort1(List<T> list) {}

static <T extends Comparable<? super T>> void sort2(List<T> list) {}

public static void main(String[] args){
ArrayList<Apple> list = new ArrayList<>();
sort1(list); //编译错误
sort2(list); //合法
}

java里还有一些高级特性,不过适用范围比较局限

交类型

1
2
3
4
5
6
7
8
9
10
11
class A{
void ha(){}
}
interface B{
void he();
}

<T extends A & B> void fun(T t){
t.ha();
t.he();
}

由于Java本身不支持多继承,交类型里只能有一个class而且只能放在第一位。

并类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A extends Exception{
}
class B extends Exception{
}

void fun(){
try {
if(flag)
throw new A();
else
throw new B();
} catch (A | B a) {
a.printStackTrace();
}
}

只能在异常块中使用,表示可能出现的异常类型。