随手
看到了这篇关于Java到底哪里出了问题的文章,笑傻了23333
“Java developers just can’t help themselves it seems - give em an inch, and next thing you know you’re looking at a OO hierarchy 15 layers deep and instantiating a hammer hammer factory.”
继承
- Java中的继承用extends,所有的继承都是public的,没有C++里那些乱七八糟的private/protected继承
- 和C++一样,子类不能直接访问父类的private field,一般用public的setter/getter去访问
- 需要调用父类的方法时,用super.method(),public的field也可以这样访问。注意super不是一个引用,只是给编译器用的一个指示而已,所以不能使用super.super(而且super.super实际上是破坏封装的,参见Why is super.super.method(); not allowed in Java?)
- 在构造函数里调用父类构造函数只要用super(...)就行了,类似this(...),必须写在构造函数第一行(在C++里还是用initializer list= =)。如果在构造函数里没有写明调用哪个父类的构造函数,会默认调用没有参数的super()(此时如果父类有其他带参数的构造函数,编译器会报错)
- 父类的数组或是变量可以放子类的引用,比如
SuperClass[] a = new SuperClass[10];
a[0] = new SubClass();
这样就可以得到多态了。在Java里会默认使用dynamic binding,和C++那种需要特意指明virtual才能启用的做法相反。
- 对于不想要dynamic binding的方法,需要声明为final,这时子类无法override这个方法(编译器报错)。如果一个类声明为final,那么它无法被继承(相当于声明所有方法为final,注意一个类为final和它的field是否final无关,它的field依然可以可变)。(比如String就是一个final class。设为final通常是不希望子类能够把它的含义玩坏,这时一个String的引用里只可能是一个String的对象)
- Java不支持多重继承,而是用interface来得到类似多重继承的效果(侧重于behavior而非identity,其实多重继承从逻辑上看通常本身就很囧,这种用interface来声明what it does而不是what it is的做法显然更make sense)
- 当然了,对于放在父类变量里的子类对象,是不能调用子类才有的方法的2333 人家才没那个闲心还要多帮你检查可能无数种多出来的子类方法呢
- Java支持父类和子类数组之间的赋值,你可以
Subclass[] suba = new Subclass[10];
Superclass[] supa = suba;
把一整个数组赋值,底层实际上还是同一个引用,Java会记得这个数组最开始是什么类的,所以如果你想趁着这个数组现在表面上是父类,想拿一个父类对象塞进去的话
supa[0] = new SuperClass();
会报ArrayStoreException(因为Superclass is not a Subclass, 这样干和 suba[0] = new SuperClass() 没区别)
- 如果想确保自己在override而不是写一个新的方法,可以加上@override在函数声明前,让编译器帮自己检查父类是否有同signature的方法,避免自己粗心大意写错了。
Dynamic Binding
- 当多态和重载混在一起的时候,到底调用哪个方法就比较蛋疼了。Java的Dynamic Binding(为一个存在继承的类的对象选择需要调用的方法)的主要步骤为
- 编译器检查子类父类所有同名的方法,先不管参数类型,总之都记下来
- 编译器检查调用时传入的参数类型(overloading resolution)。如果第一步记下来的方法里只有一个符合这个参数列表,就调用它,如果没有一个方法匹配,就报错。如果将参数进行转换之后,一个类里有多个方法匹配,也报错
- 如果被调用的方法是private/static/final/constructor,那么编译器直接是什么类就调用什么类的方法,不去追究继承(子类),此为static binding
- 如果非第三步中的情形,就需要做dynamic binding了,调用实际引用的类的方法(可能会调用到子类的方法)
- 总之就是overload先于override,此外VM为了节省时间不会每次调用方法都这样搜,会先打表。如果是nonstatic方法,子类的表里没有override的方法会连到父类的方法去,然后在找方法的时候VM直接先获取这个引用的实际类型,然后得到这个类型的表。如果是static方法,不会获取实际类型,直接用引用类型。
- override的要求是父类和子类的方法有一样的signature。注意虽然return type不算在signature里,但是一般需要保证被override的方法依然和原来的方法兼容 e.g. 子类的方法要么返回和原来一样的类型,要么返回原来类型的子类(这时叫做covariant return type)。此为一般要求override的方法的可见性不能低于父类,原来是public的不能改到private,会被编译器抱怨。
- 添加新类的时候,父类和其他子类的代码不需要重新编译。
- 曾经有不少人以为用上final避开dynamic binding可以减少overhead(因为CPU的branching容易打断硬件层面的优化e.g. pipelining,而dynamic binding会让编译器无法确定一个方法是否可以inline,造成运行时需要branching),但是现在的JIT可以推到运行前再决定是否inline,不需要手工干这种优化了。
Casting
- 子类放进父类可以直接放
sup = sub;
而父类放进子类呢?可能可以,只要它一开始是这个子类,可以被cast回来。Java对象的cast语法上像是C原生的static cast,但实际上原理和C++的dynamic_cast才是一路的:
sub2 = (SubClass) sup;
注意这里一定要做cast,不能直接赋值回去,因为编译器会检查你是否promise too much,直接赋值(sub2 = sup)就是这样一种表现。如果你做了cast(且cast的类型和引用类型存在继承关系),检查会推迟到运行时,如果运行时检查出实际类型不能被cast回来,才会报ClassCastException(C++不能cast的时候是返回null而已)。 - 所以你可以有
- 一开始为父类的数组,中间塞几个子类对象,然后再把这几个子类对象拿回来的时候cast回子类
- 一开始为子类的数组,赋值成一个父类数组,但是中途不能塞父类对象进去
- 为了防止ClassCastException挂掉整个程序,如果选择不为这个exception加handler,一般会在cast前先用instanceof检查是否可以cast
if (sup instanceof SubClass) {
sub2 = (SubClass) sup;
}
- 如果被cast过去的类型跟原来的类型根本就没有继承关系,那么会在编译时就报错。
- null和instanceof一起用也不会抛异常,会返回false
- 如果不是需要再调用子类特有的方法,没必要cast回去,dynamic binding已经会保证调用子类方法了。
abstract class
- 和C++的概念一样,当一个类太抽象了以至于根本不需要实例化,只要被其他类继承再实现的时候,可以将它设为一个abstract class
- 设定为abstract class需要
- 在类声明里加上abstract
- 至少要有一个方法的声明里有abstract,abstract的方法不能有实现
- 和C++的不同
- abstract class
- Java需要用abstract class声明这个类
- C++没有特别的语法声明abstract class,只要存在纯虚函数即可
- 纯虚函数
- 举例:
- Java:public abstract void f();
- C++:virtual void f() = 0;
- Java的abstract方法不能有实现,C++的纯虚函数可以有实现。
- C++的纯虚函数实际上是靠那个 = 0来标明的,virtual是用来声明可以进行override的。
- 可被override的具体方法:C++需要加上virtual
- Java: public void f() {...}
- C++: virtual void f() {...}
- 不可被override的具体方法:Java需要加上final
- Java: final public void f() {...}
- C++:public void f() {...}
- 继承abstract class的子类不一定需要实现所有的abstract方法,但是如果有留下没有实现的abstract方法,子类也是abstract的,也必须用abstract class声明。
- 没有abstract方法的类也可以是abstract class = =!和普通类的区别只不过是不能实例化了而已。
- abstract class的不能实例化指的是不能和new一起用而已,但是可以用来声明变量
AbClass a; // OK!
AbClass a = new AbClass(); // Go to hell!
AbClass a = new NonAbSubClassOfAbClass(); // OK
- abstract方法存在的主要意义是一个方法可能太抽象了,在父类里很难能有自己的实现,但如果不在父类里实现它,只有子类有,那么就不能利用多态去调用它(因为这时它是属于子类特有的方法),而abstract方法可以让它只有一个位置而不需要具体实现,留到多态的时候可以调用。
- abstract方法主要在interface里用。
Protected
- 将访问权限扩大到子类和其他所有同package的类,后面这点和C++不同
- 一般来说,protected的方法比protected的field更make sense,因为如果把field的访问权限也放出去了,类实现者为了不坑用这个类的人,就不能随意删改这些field了,实际上是打破了封装。而如果是包在protected的方法再放出去,那么实现者可以在改掉底层实现后,通过一点hack保证以前的protected方法还能用,在为新的更好的实现提供新的接口的同时,不破坏原来的兼容性,毕竟deprecated总是好过broken的。
Object
equals
- 和 JS 一样,== 实际上比较的是引用在内存中对应的位置,需要比较值是否相同,需要用equals
- 注意 equals 的参数是 Object,在子类里override的时候也是这个,所以声明是
public boolean equals(Object obj) {...}
- 在比较的时候如果怕其中一方是null会抛异常,可以不用 a.equals(b) 而用 Objects.equals(a, b),或者先手动检查
- 关于equals的实现通常有几点要求,参见Object.equals的javadoc(因为一切类都继承自Object,所以子类对equals的实现也应当符合Object的equals的要求),另外有一个Stackoverflow上的讨论值得一看
- equals的symmetric要求有一个坑:很多人喜欢用instanceof而不是getClass来判断两边是否为同一个类(instanceof对于同一个父类的都为true),此时sup.equals(sub) 就是true。如果还要符合Java对symmetric的要求,sub.equals(sup)也要为true,此时用instanceof是无法实现的,因为instanceof本身就是一个asymmetric的运算,后者肯定为false。有人搬出Liskov substitution principle,说sup.equals(sub) 本来就应该为真,也就是子类和父类的比较应当无视子类特有的那些属性,一个子类的对象可以和一个父类的对象相等。这是一个鱼与熊掌不可兼得的情况:要么你就用getClass违反Liskov substitution principle,要么你就用instanceof违反Java对于equals的要求,没有一种实现能够兼顾两者。
- 一个常见的支持sup.equals(sub)的例子:AbstractSet可以有TreeSet和HashSet两种实现,那么两个元素相同的set,abSet.euqals(hashSet)应当为true,因为实现是否是否相同在逻辑上讲是和两个对象的元素是否相等无关的。
- 作者的观点是
- 如果子类对相等的定义和父类不同,就用getClass确保两者同类,比如常见的“所有field相等就相等”,而子类会多出来一些field,那么定义就是不同的
- 如果相等的定义已经在父类确定了,就用instanceof,并且在父类的equals声明用final,比如上面的实现与相等无关的set(相当于只要逐个检查field且不去管多出来的field),或者父类已经有一个唯一确定的ID的field并且子类也用这个ID来判断相等。
- 实现equals的基本步骤
- 声明为
public boolean equals(Object obj) {...}
这里参数类型不能替换成其他,换了就signature和Object的equals就不同了,不是override而是添加新的方法。
- 首先检查是否为同一个对象,因为检查引用位置的开销比较低
if (this == obj) return true;
- 检查obj是否为null
if (obj == null) return false;
- 检查类,按照前面说的子类和父类对相等的定义是否相同来决定怎么写
- 子类的相等定义和父类不同,那么两者必须同类
if (getClass() != obj.getClass()) return false;
- 相等的定义已经在父类确定,那么这就是父类里final的equals,另一个对象需要至少是这个类的子类
if (!(obj instanceof ClassName)) return false;
- 检查之后已经确定另一个对象是这个类或者是这个类的子类,可以放心地cast了,接着就是将另一个对象cast后逐个field对比或者检查ID之类的。注意如果field是对象,对比要用equals()或者Objects.equals(),如果field是数组,对比要用Arrays.equals()
- 注意,如果子类的相等定义和父类不同,也就是用getClass的情形,那么在equals的第一句通常还要写上
if (!super.equals(obj))
return false;
确保cast之后在父类的level上相同。
hashCode
- String的hashCode是
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
(和我当年写的一个hash函数好像啊= =!)参见String的API - Object的API文档里讲解了hashCode和equals的联系(如果equals为真,hashCode一定相等,如果equals为假,hashCode不一定不相等),但实际情况下Object的hashCode是从内存位置算出来的,如果内容相同而位置不同,返回的也是不同的hashCode,没有override hashCode的类都直接用Object的hashCode,所以想要让hashCode和内存位置无关的话需要自己override。另外如果equals被override了,而且这个类可能会被用在哈希表里,那么你也要负责override掉hashCode。
- hashCode可以返回负数
- 如果想调用Object自带的hashCode,用Objects.hashCode()更安全,因为它是null-safe的
- Objects.hash(a, b, c, ...) 可以调用多个对象的hashCode并将它们组成一个新的hashCode,这样在给子类写hashCode的时候就轻松很多了。
- 原始类型的数组可以用Arrays.hashCode来算hashCode
toString
- Object自带的toString返回的是
getClass().getName() + '@' + Integer.toHexString(hashCode()) - 一般toString返回的格式是
package.class[field1=a,field2=b,...]
- 当一个对象和+连用的时候,Java会自动调用这个对象的toString,然后把返回的字符串连起来(注意Java里除了String其他所有对象都没有+,而且Java没有运算符重载),所以 x.toString() 和 "" + x 得到的是一样的值。
- System.out.println(x) 会调用x的toString
- 注意数组自带的toString是继承自Object的(先是[表示数组,然后一个字母表示元素类型,然后一个@,然后是hashCode),要将数组元素用逗号分隔中括号围住打出来用的应该是Arrays.toString,多维数组用Arrays.deepToString
Array List
- 类似于STL的vector,其实下面还是一个定长的数组,但是会按照一定的策略重新分配底层内存来达到可变长度的效果。
- 写法
ArrayList<T> a = new ArrayList<T>();
右边的T可以安全地省略,因为编译器发现找不到参数会去看左边。
- 在Java 5以前是没有泛型的,所以没有,都是raw type,声明就写ArrayList a就好了(就是之前在龙书里看到的那种囧TZ),这时候get到的都是Object要手动cast,set的时候编译器也不查传进去的参数类型是不是和ArrayList匹配,要到运行时才跪,所以很危险。
- Java里还真的有一个Vector……不过效率不如ArrayList,已经deprecated
- 添加元素用add,有两种,分别可以加到尾巴或者加到指定位置
- ensureCapacity可以用来重新分配指定内存,trimToSize用来根据当前大小重新分配内存
- 构造函数里可以传一个数字作为初始大小的预估
- C++的vector的=是copy-by-value,会给新的vector重新分配内存,而Java的ArrayList只是将引用指向同一块内存而已。
- 因为Java没有运算符重载,ArrayList不能用[],获取元素用get,修改元素用set,删除元素用remove
- 数组和ArrayList的转换用Arrays.asList和toArray
T[] a1 = new T[10];
ArrayList<T> a2 = new ArrayList<T>(Arrays.asList(a1));
T[] a3 = new T[a2.size()];
a2.toArray(a3);
- 和STL的vector一样,ArrayList的set、get和根据下标获取元素都是O(1),但是从中间或者前面增删元素是O(n),如果需要经常做类似操作,需要用LinkedList
- 数组获取长度用a.length,ArrayList用a.size()
- ArrayList也支持 for(ele : a) 这种便利方式
- 通过传参数的方法将一个带类型的ArrayList转化成raw type的ArrayList是没有warning的,除非编译时加上-Xlint:unchecked。因为Java编译器本身在检查完类型之后就会将所有带类型的ArrayList转化成raw type的ArrayList,所以JVM上所有的ArrayList其实都是raw type的。
- raw type的ArrayList转化成带类型的ArrayList就一定会有warning,无论是赋值还是cast,除非加上@SuppressWarnings("unchecked")
Wrappers
- 泛型的类型参数不能是原始类型,需要用原始类型的wrapper : int对应Integer,double对应Double, .etc
ArrayList<int> a; // WRONG
ArrayList<Integer> a; // OK
- wrapper们是immutable+final的
- 注意ArrayList<Integer>的效率远远低于int[]
Autoboxing
- 原始类型在需要的时候会被自动转换成wrapper类型,比如list.add(3) 会被编译器自动转换成 list.add(Integer.valueOf(3))
- wrapper类型在需要的时候也会自动转换成原始类型,比如int n = list.get(i) 会自动转换成int n = list.get(i).intValue()
- 除了函数调用以外,在使用运算符的时候也会有autoboxing和autounboxing
- 唯一的例外是==,这个时候编译器不会自动转换类型,如果是int就比较值,如果是Integer就比较内存位置。
- JLS 5.1.7规定了boolean,byte,小于127的char,-128~127的int放在固定的内存位置,所以这些值的wrapper用==判断都会返回真,但一般还是推荐不要依赖这个规定,用equals判断相等
- boxing和unboxing是编译器做的,所以在bytecode层级上就已经确定了
- 虽然wrapper是对象,但是因为它们是immutable的,所以即使引用pass by value传进来,也不能在方法里修改对象内部的值。如果想要修改内部的值,需要用org.omg.CORBA的placeholder们来做。
不定数目的参数
- 可以用T...放在声明里表示不定数目的参数,使用时直接当做T[]使用。这种参数只能有一个,并且必须放在最后。
- 如果传入的是原始类型,在方法里的T[]会自动被转换成wrapper类型
- 对于有不定数目参数的方法,可以传入数组来代替这些不定数目的参数
Enumeration
- 声明方法类似于
public enum Name {ENUM1, ENUM2, ...};
- 每种类型分别是Name.ENUM1,Name.ENUM2...
- 每个enum实际上是一个类,有且只有声明里的那些类型那么多的实例,所以比较的时候可以用==,因为同一种类型一定对应同一个对象
- enum的类型可以有自己的field,方法,构造函数
public enum Name {
ENUM1, ENUM2, ... ;
private int field ...;
private Name(...) {...}
}
- 各种enum都是Enum的子类,enum中的类型的toString会返回这种类型的名字,Enum.valueOf会将对应名字的类型返回来。
- 名字与enum的转换
Name.ENUM1.toString(); // "ENUM1"
Enum.valueOf(Name.class, "ENUM2"); // enum with type ENUM2
- Name.values() 会返回这个enum的所有类型的值([Name.ENUM1, Name.ENUM2...])
- ordinal()返回某个enum类型在这个enum集合里声明的位置。
Reflection
- Java的黑魔法,少碰为妙(说好的简化版C++呢
- getClass用于在runtime获取类的实际类型
- getClass().getName()获取类名字符串(包括package名)
- Class.forName()获取名字对应字符串的类
- T.class返回类型T对应的Class对象,注意一种类只有一个Class对象,所以可以用==
- getClass().newInstance()创建指定类的新对象,调用的是无参数的构造函数
- Class类似C++的type_info,getClass()类似C++的typeid
- 更多关于reflection的文档可以查看Class,Field,Method,Constructor,Modifier
Hints
- 尽量少用protected field,改用protected方法
- 子类的继承没有限制,所以依然可以破坏封装
- Java里同一package的类都可以访问protected的field,进一步弱化了protected的作用
- 如果没办法符合“is-a”关系,就不要勉强继承了,可以改用interface
- 如果有方法不能在子类上closed,就不要用继承。
- override方法的时候不要改变它的behavior(尤其注意是否符合substitution principal),否则先考虑设计是否有问题
- 用polymorphism而不是type polymorphism,举例来说,当你看到
if (x is of typeA)
action1(x)
else if (x is of typeB)
action2(x)
如果action1和action2的behavior存在重叠,最好重新考虑是否设计一个公共的父类或者interface,用多态来避开这种if-else test
- 不要乱用reflection = =