Thinking in Java阅读笔记
Initialization & Clean up
Static data initialization
- 当新建一个类或者直接使用这个类里的static变量的时候,这个类里所有的static变量就会被初始化,static变量先于non-static变量进行初始化,并且整个程序中只有一份static变量的instance(即被所有对象共享)
- static block: 跟static变量一样,随着类的加载而执行,只执行一次,并优先于主函数,可用于给类进行初始化
Access control
public>protected>package(default)>private
public
所有人都有access
protected
当前package或者继承类有access
package(default)
只有当前package的其他class有access。需要注意的是,liushiy.package1和liushiy.package2是两个不同的package,即使某个variable在liushiy.package1是package access,我们在liushiy.package2是不能使用的
private
只有当前这个class可以使用
Reusing Classes
Composition
- Composition(组合)是最常见的复用类的方法,当我们创建一个新类的时候,里面包含旧类的对象
- 组合是一种HAS-A的关系,新类中有旧类
Inheritance
Inheritance(继承)是IS-A的关系,子类是一种父类
继承的关键字是extends,子类自动继承父类所有的属性(fields)和方法(methods)
子类并不局限于使用父类的fields和methods,可以创建自己的fields和methods
子类可以重写(override)父类已有的methods。重写是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!重载(overloading)是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。
子类进行重写的时候,可以在重写的方法上加上@override
注解。@override
注解不是必须的,加上的好处是编译器会确认注解后面的方法确实是在重写。可以避免诸如方法名写错(编译器会认为这是子类的新方法,而不是在重写父类的旧方法),或者想要重写但不小心写成了重载如果子类重写了父类的某个方法, 但是又想使用父类的这个方法,那么我们可以用super关键字去call父类的方法。下面这个例子的输出是: Fake Miao
1
2
3
4
5public class Animal {
public void call(){
System.out.println("Fake");
}
}1
2
3
4
5
6
7
8
9
10
11
12public class Cat extends Animal{
public void call(){
super.call();
System.out.println("Miao");
}
public static void main(String[] args) {
Animal animal = new Cat();
animal.call();
}
}子类在初始化的时候,会先自动调用父类的default构造器(constructor)去初始化父类的属性。如果父类没有默认constructor, 那么子类必须使用父类其他的constructor去初始化属性,即我们需要在子类中使用super关键词去call父类的constructor,并且super关键词必须在子类constructor的第一句。 如果父类既没有默认构造器,我们又不在子类调用某个父类的构造器,那么编译器会报错: There is no default constructor avaliable
1
2
3
4
5
6
7
8
9public class Animal {
private String name;
public Animal(String name){
this.name = name;
}
public void call(){
System.out.println(name);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public class Cat extends Animal{
public Cat(){
super("Fake");
}
public void call(){
super.call();
System.out.println("Miao");
}
public static void main(String[] args) {
Animal animal = new Cat();
animal.call();
}
}关于第六条,我们可以理解为初始化子类对象的时候,会同时在其中创建一个父类的subobject,本质上的理解:https://www.zhihu.com/question/51920553
继承类的初始化顺序:父类static initialization->子类static initialization->父类fields被设置成默认值->子类fields被设置成默认值->父类constructor->子类constructor
The Final keyword
Final keyword means ‘This can not be changed’ in general. You might want to prevent cahges for two reasons: design or effciency.
Final data
Final data分为两种,一种是final primitives,一种是final object reference。Final primitives代表着这个field的value就是不可变的,final object reference代表这个field的reference不可以再变了,但是它的value是可以变的
定义为final的fields是可以没有被初始化的,但是在使用这个field之前一定要完成初始化
Final arguments
Final arguments means that inside the method you cannot change what the argument reference points
Final methods
Final methods的作用是防止子类override
Final and Private
private method in a class are implicitly final. 因为private method只有自己的类中能看到,即使是子类也看到父类的private method,更不能够override它
Final classes
Final class不能被继承,并且final class里的methods自动是final的(因为不能继承,也就谈不上override了)
Polymorphism(dynamic binding/late binding/run-time binding)
Upcast: 从子类转成父类,总是安全的; downcase:从父类转成子类,需要加括号并确认要转的确实是这个子类
Method-call binding
Early-binding: 在compile time,编译器只会根据某个对象的表面上写的是什么class从而判断他能不能call某个方法,如果不能的话会报compile-error
Late-binding:在run time,编译器会真的决定这个对象到底是什么class(而不是根据他表面上是什么class),并使用它真正的class里的那个method去call
比如Shape s = new Circle(); s.draw();
,compile time,编译器只会检查Shape类有没有draw这个方法,没有的话报错,run time,当我们想要使用s对象的draw method的时候,编译期才会真正去查看s对象真是的类,虽然他被定义为Shape类,但真实的类是Circle,所以最终会调用Circle类里的draw方法
总结来说,compile time看等号左边,run time看等号右边
几个不支持多态(dynamic binding)的例子
- overriding private method
- fields:fields不存在override也就没有多态,不会使用dynamic binding,会使用等号左边的class里的field。子类和父类是可以有相同名字的field的,子类默认使用本身类的field,如果想使用父类的field,需要加上super
- static methods: static method是和class绑定的,不会使用dynamic binding,会使用等号左边的class里的static method
Interface
Abstract classes and methods
- 抽象方法指的是一个方法没有具体的实现,比如
abstract void f()
。 一个类中如果有一个或多个抽象方法,那么这个类我们必须声明成抽象类abstract class test(){}
- 抽象类不可以声明相应的对象,抽象类是为了继承使用的。当我们继承一个抽象类的时候,子类可以override抽象类中的抽象方法,也可以继续不实现这个抽象方法,而是继续声明成抽象方法,留给这个子类的子类去实现
- 抽象类可以一个抽象方法都没有,即都是实现好的方法,这个时候使用抽象类的目的就是不让这个类可以声明对象
Interfaces
- The interface keyword produces a completely abstract class, ont that provides no implementation at all. it allows the creator to determine method names, argument lists and return types, but no method bodies
- We can define fileds in interface, but these are implicitly static and final
- Interface里定义的methods即使我们不定义为public,也都自动都是public
- java中一个类只可以有一个父类,但是可以有多个interface,这就使得多态的用法更加的灵活。一个类可以继承多个interface(An x is an a and a b and a c),当他需要变成哪个interface类型的时候,他就可以成为那个interface类型
Inner class
- If you want to make an object of inner class anywhere except from whin a non-static method of the outer class, you must specify the type of that object as OuterClassName.InnerClassName
- Inner class在声明对象时,ourter class的对象必须已经存在,我们不可以用
OuterClassName.InnerClassName test = new OuterClassName.InnerClassName()
来单独声明Inner class的对象,inner class’寄生于’outer class中,只有outer class有了对象,inner class才能有对象 - 当我们想要在inner class使用outer class对象的reference的时候,我们可以使用
OuterClassName.this
来指代 - 当我们已经有了outer class的对象,想要声明inner class的对象的时候,我们可以用
OuterClassName outerClass = new OuterClassName(); OuterClassName.InnerClassName innerClass = outerClass.new InnerClassName();
来声明
Inner class and upcasting
使用inner class和upcasting,可以起到很好的封装效果,下面的例子对于Cat类,我们想要有一个比较器并且这个比较器只存在于Cat类中并且不能被别人修改,这种情况下我们可以定义一个private inner class
1 | import java.util.Comparator; |
1 | import java.util.Comparator; |
值得注意的是,这里的我定义的inner class CatComparator是private,但是在implement comparator后,它override了一个public的方法,所以出现了在private类里面存在一个public方法:这是合情合理的,类的visibility和方法的visibility没有任何关系,两者是完全独立的。这里我们不想让别人修改我们的CatComparator类的定义,所以定义为private,同时我们也需要使用compare方法去比较猫的年龄,所以compare方法定义为public也是合理的
Anonymous Inner class
匿名内部类就是在类中创建匿名的内部类,语法是当我们new一个对象的时候,我们说等等,我想要继承这个对象的类,并且new这个新生成的匿名内部继承类。这样做的好处是这个匿名内部类只被我们使用一次,不会被client engineer误用。所以上面的例子我们可以用匿名内部类简化成下面这样子:
1 | import java.util.Comparator; |
1 | public class Test { |
如果我们不只是想单单call父类的构造器,我们还想要写子类自己新的构造器,这个时候我们可以使用instance initialization:
1 | public class Wrapping { |
在java8以前没有effective final概念之前,如果我们要在匿名类里面用到外面的参数,那个参数必须声明为final: https://www.zhihu.com/question/21395848
但是在java8以后就不需要了,所以我们上面例子里不用定义成final int x
,编译器也不会报错
Nested Class
Nested Class是一种特殊的inner class,即我们把inner class声明为static。之前在inner class部分我们提到过,要声明inner class,我们必须先声明它的outer class对象(即inner class是寄生于outer class的)。但是对于Nested Class(Static Inner Class),就没有这样的要求,我们可以直接声明inner class的对象。
A nested class means: 1. You don’t need an outer-class object in order to create an object of a nested class
2.You can’t access a non-static outer-class object from an object of a nested class.
Classes inside interface
正常来说,我们不能在interface里面放一个class(因为interface本来就是用来做抽象的),但是我们可以把nested class放到interface里去。对于interface来说,它内部的东西默认必须是static final的,而我们的nested class是满足这个条件的。
It’s convenient to nest a class inside an interface when you want to create some common doe to be used with all different implmentations of that interface
Why inner classes?
The most compelling reason for inner classes is :
Each inner class can independently inherit from an implementation. Thus, the inner class is not limited by whether the outer class is already inheriting from an implementation.
所以inner class给予了我们继承多个class的能力,同时还能方便的使用outer class的fields和methods
Holding your object
Collection
Arrays.asList
生成的List可以改变其中的元素,但不能向里面增加元素,因为本质上它是由Array生成的。如果又想使用这个方法又想增加新的元素,那么需要声明一个新的ListList<Integer> test = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5))
- Collection包括List, Set, Queue, Stack, PriorityQueue
Iterator
- 所有的Collection都有一个iterator() method,生成一个Iterator对象,这个对象有hasNext(), next(), remove()三个方法,我们可以使用这三个方法对Collection进行遍历。
1
2
3
4
5
6
7Collection<Integer> c = new LinkedList<>();
Iterator it = c.iterator();
while(it.hasNext()){
Interger i = it.next();
System.out.print(i);
it.remove();
} - Collection有iterator() method是因为所有Collection都实现了Iteratable接口
- 实现了Iteratable接口的类就可以使用foreach循环了,所以本质上foreach循环的底层是调用Iteratable.iterator()。这也解释了为什么Map不能用foreach,因为Map不是Collection,没有实现Iteratable,我们必须用
for(Map.Entry<Integer, integer> entry:map.entrySet())
拿到entry set以后才可以进行遍历。
Error Handling with Exceptions
Basic exceptions
- Exception对象的建立和普通java对象的建立完全一样:使用new,并且建立在堆(heap)上。当我们想要在某个地方抛出异常的时候,我们使用
throw new Throwable();
这样的语法结构。 - Throwable对象是所有可以抛出的异常的源头父类。Throwable本身除了默认构造器外还有4个构造器,这五个构造器中的两个是所有JDK定义的标准异常都有的构造器:默认构造器和接收String的构造器。比如NullPointerException就只有这两个构造器:
1
2
3
4NullPointerException()
//Constructs a NullPointerException with no detail message.
NullPointerException(String s)
//Constructs a NullPointerException with the specified detail message.
Catch an exception
- try-catch block
1
2
3
4
5
6
7
8
9try {
//Code that might generate exceptions
} carch (Type1 id1) {
//handle exception of Type 1
} carch (Type2 id2) {
//handle exception of Type 2
} carch (Type3 id3) {
//handle exception of Type 3
} - 当异常发生的时候,程序会去寻找第一个符合抛出异常类型的handler,并进入到那个catch块进行异常处理。这一要说明try-catch和switch有很大区别,switch必须要有个break告诉程序退出执行,否则switch会把每一个选择块都执行一遍。
Creating your own exceptions
- 如果在程序中的exception我们没有使用try-catch进行处理,或者我们自己在程序中抛出了某些异常,我们有义务告诉client programmer我们抛出了哪些异常,这样方便他们在使用我们的程序的时候去检查这些异常。所以Java引入了异常说明(Exception Specification),也就是我们需要在方法外说明我们在这个方法里抛出了哪些异常:
void f() throws TooBig, TooSmall, DivZero {}
- Throwable由两个子类继承: Error类和Exception类。Error类是一些程序运行中本身遇到的错误,比如线程死掉了;Exception类是我们需要处理的,它又分为Checked Exception和Unchecked Exception。 Checked Exception是那些我们写代码的时候必须处理的异常,我们必须使用try-catch处理这个异常或者使用异常说明告诉程序继续抛出这个异常,否则编译器会报错。
Unchecked Exception是那些在Runtime可能发生的异常,属于我们不用主动处理的异常,Unchecked Exception是那些继承自RuntimeException(包括)的异常。
Catching any exception
Throwable类本身有一些很有用的方法:
1
2
3
4
5
6
7
8
9//Gets the detailed exception message
String getMessage()
string getLocalizedMessage()
//Returns a short description of Throwable, including the detail message if there is one
String tostring()
//Prints the Throwable and the Throwalbe's call stack trace
void printStackTrace()getMessage()是tostring()的子集;tostring()是printStackTrace()的子集
处理异常一个非常常见的方式就是rethrow:
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
32public class Main {
class RootException extends Exception{}
class CausedException extends Exception{
}
public static void main(String[] args) {
Main main = new Main();
main.h();
}
public void h(){
g();
}
public void g(){
try {
f();
} catch (CausedException e) {
e.printStackTrace();
}
}
public void f() throws CausedException{
try{
throw new RootException();
} catch (RootException e) {
CausedException causedException = new CausedException();
throw causedException;
}
}
}最后打印出来的异常是:
1
2
3
4
5Main$CausedException
at Main.f(Main.java:30)
at Main.g(Main.java:19)
at Main.h(Main.java:14)
at Main.main(Main.java:10)Exception chaining: 当我们rethrow一个新的一场的时候,我们往往想要保留原异常的信息。比如上个例子中的CauseException是由RootException引起的,但是最后打印出来的异常信息中,我们完全找不到RootException了。这个时候我们就需要使用异常串。异常串的原理就是我们在抛出一个新的异常的同时,告诉它造成这个新异常的cause,这样程序就会知道是谁造成的这个新的异常。Throwable本身是有一个接受cause参数的构造器的
Throwable(Throwable cause)
。但是这个构造器只被Error,Exception和RuntimeException继承了,比如NullPointerException就没有这个构造器,那么如果我们想要NullPointerException知道它的cause,我们就需要使用initCause()
这个方法。如果是我们自己定义的异常,我们可以选择重写Exception的带cause参数的构造器,那么样我们就可以直接使用了。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
32
33
34
35
36
37public class Main {
class RootException extends Exception{}
class CausedException extends Exception{
public CausedException() {
}
public CausedException(Throwable e) {
super(e);
}
}
public static void main(String[] args) {
Main main = new Main();
main.h();
}
public void h(){
g();
}
public void g(){
try {
f();
} catch (CausedException e) {
e.printStackTrace();
}
}
public void f() throws CausedException{
try{
throw new RootException();
} catch (RootException e) {
CausedException causedException = new CausedException(e);
throw causedException;
}
}
}打印出来的结果是:
1
2
3
4
5
6
7
8Main$CausedException: Main$RootException
at Main.f(Main.java:35)
at Main.g(Main.java:24)
at Main.h(Main.java:19)
at Main.main(Main.java:15)
Caused by: Main$RootException
at Main.f(Main.java:33)
... 3 more
Performing cleanup with finally
- finally块里的语句总是会执行,即使try块里有break,continue或者return
- finally总是执行的机制会导致一个坏处:lost exception。比如下面这个例子: 或者
1
2
3
4
5try {
throw new ImportantException();
} finally {
throw new NotImportantException();
}上面两个例子都是导致我们丢失ImportantException1
2
3
4
5try {
throw new ImportantException();
} finally {
return;
}
Exception restrictions
- 当子类重写父类的方法时,子类重写的方法只能抛出父类方法已有的异常或已有异常的子异常。 这个是很好理解的,如果子类方法抛出了父类方法所没有的异常,那么如果我们使用多态(upcast)的时候,编译器认为父类方法没有抛出新增加的异常,但其实在runtime,子类抛出了该异常,这样程序就无法处理了。所以Java严格要求子类重写的方法只能抛出父类方法已有的异常或已有异常的子异常。
- 总结上面一条就是: 异常的继承只能变得更窄,而不能变得更宽。这正好和类的继承相反,类的继承只能变得更宽,因为父类的变量和方法子类默认已经继承,我们只能重写父类已有的方法或者增加新的方法。
- 所以子类重写的方法是可以不抛出异常的,即使父类抛出了某些异常(变窄了)
- 子类中新定义(非重写)的方法没有以上限制
- 构造器不用遵循上述的限制,即可以随便抛出不同于父类构造器的异常,但是子类所使用的父类构造器的异常一定要继承(这里强调所使用的父类构造器的原因是子类可以不调用默认构造器,但是无论如何子类必须调用一个父类的构造器,否则编译器会报错)。构造器不用遵循上述规则的原因也很简单,构造器不像方法有多态,构造器总是出现在等号的右边。
Exception matching
- Catch异常的时候不一定是完美符合,即子异常也会被父异常的catch语句抓到。所以我们总是在try-catchd的最后补上
catch(Exception e)
,而不能在最开始写。
Strings
Immutable Strings
- String object都是不可更改的(immutable),所有对于String的操作都是生成一个新的String,而非更改旧的
Overloading ‘+’ vs. StringBuilder
- Java编译器在处理String的加操作时,底层其实也是在使用StringBuilder的。但是在遇到循环的时候,编译器会生成多个StringBuilder在每一次循环的时候。
- 所以对于String和StringBuilder的使用:简单的加操作直接用String,涉及到循环的时候一定要用StringBuilder
- StringBuilder是java1.5引入的,之前使用的是StringBuffer,原理与StringBuilder基本相同,但是StringBuffer是线程安全的,而stringBuilder是线程不安全的,所以这就导致StringBuffer比StringBuilder效率要低
Formatting output
- Java中的format相关实现都是由
java.util.formatter
来完成的。常用的String.format()
的内部实现也使用的formatter - Formatter的使用见: https://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html
Regular expressions
- 在其他语言中,
\\
表示:我想要在正则表达式中插入一个普通的(字面上的)反斜杠,请不要给它任何特殊的意义。在 Java 中,\\
表示:我要插入一个正则表达式的反斜线,所以其后的字符具有特殊的意义。所以,在其他的语言中(如 Perl),一个反斜杠\
就足以具有转义的作用,而在 Java 中正则表达式中则需要有两个反斜杠才能被解析为其他语言中的转义作用。也可以简单的理解在 Java 的正则表达式中,两个\\
代表其他语言中的一个\
,这也就是为什么表示一位数字的正则表达式是\\d
,而表示一个普通的反斜杠是\\
。 - 其他关于正则表达式的表达可以参考: https://www.liaoxuefeng.com/wiki/1016959663602400/1017639890281664
- Java中关于正则表达式的内部实现都是由Pattern类和Matcher类完成的,这里有时间可以好好看一下
Type Information
Run time information(RTTI)分为两部分。传统的RTTI假定我们在编译时已经知道了所有的类型信息;另一种反射机制允许我们在运行时发现和使用类型信息。
The Class object
- 平时我们生成Java对象的时候都是直接用new,但new的背后其实隐藏了一些步骤。当我们写好一个新的类并进行编译时,JVM都会为这个新的类创建一个对应的Class对象,并将其存在.class的字节码中,这个Class对象会用来生成这个类的对象。
- JVM一开始运行的时候,并不是所有的类都被直接装载进来,而是动态的逐渐的装载进来。JVM使用Java Class Loader来进行类的装载:每当程序第一次使用某个类的静态成员static member(包括静态常量,静态变量和静态方法)的时候,JVM就会将这个类加载进来(类的构造器默认是静态的,这也是为什么我们使用new就会装载这个类的原因)
- 当我们想要使用某个类的静态成员的时候,Java Class Loader会先检查这个类是否已经被加载了,如果没有的话就去寻找这个类的.class字节码并进行加载,这样这个类的Class对象也就同时被加载到了内存中,后面我们就可以使用这个Class对象来创建这个类的对象
- 如果我们想要得到Class对象的引用,我们可以使用
Class test = Class.forName(FULL_QUALIFIED_NAME)
。FULL_QUALIFIED_NAME的意思是要包括包名。这个时候有个疑问:有没有可能想要得到的Class对象还没有被加载进JVM呢?这个问题很好,答案是这个方法的一个“副作用”就是如果这个类还没有被加载,那么加载它。另一个方法去得到Class对象的引用是如果你已经有一个类的对象,可以使用getClass()
方法。 - 之前说过Class对象是用来生成这个类的对象,这也就说明了Class对象存储了这个类几乎所有的信息。其中有一些常用的方法:
getName():返回类的全名(包括包名)
getSimpleName():返回类名
getInterfaces():返回一个数组,里面包括这个类的所有接口的Class对象的引用
getSuperClass():返回这个类的父类的Class对象的引用
newInstance():使用默认构造器创建这个类的一个对象。后面在反射部分会介绍怎么使用别的构造器创建对象
Class literals
- 第三种得到Class对象引用的方式是:
Test.class
。这种方式不仅效率更高,而且避免了forName()
方法可能抛出的异常。 - 基本类也都有Class literals。比如
int.class
就和Integer.TYPE
完全一样。 - 前面我们提到把字节码加载到JVM中,但这其实只是准备使用一个类的的第一步:
- 加载 加载是由Java Class Loader完成的,它的作用是把.class字节码加载到JVM中(Class对象也就被创建了,因为Class对象存在.class字节码中)
- 链接 链接会验证字节码;为静态域static field(静态变量+静态常量)分配存储空间;如果有必要的话,解析这个类创建的对其他类的引用
- 初始化 初始化父类;执行静态变量的初始化和静态代码块由于类的初始化阶段会执行静态块,所以我们可以通过判断静态块是否执行来判断类是否初始化了。从这个例子中可以看出来:
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51import java.util.*;
class Initable {
static final int staticFinal = 47;
static final int staticFinal2 =
ClassInitialization.rand.nextInt(1000);
static {
System.out.println("Initializing Initable");
}
}
class Initable2 {
static int staticNonFinal = 147;
static {
System.out.println("Initializing Initable2");
}
}
class Initable3 {
static int staticNonFinal = 74;
static {
System.out.println("Initializing Initable3");
}
}
public class ClassInitialization {
public static Random rand = new Random(47);
public static void main(String[] args) throws Exception {
Class initable = Initable.class;
System.out.println("After creating Initable ref");
// Does not trigger initialization:
System.out.println(Initable.staticFinal);
// Does trigger initialization:
System.out.println(Initable.staticFinal2);
// Does trigger initialization:
System.out.println(Initable2.staticNonFinal);
Class initable3 = Class.forName("Initable3");
System.out.println("After creating Initable3 ref");
System.out.println(Initable3.staticNonFinal);
}
} /* Output:
After creating Initable ref
47
Initializing Initable
258
Initializing Initable2
147
Initializing Initable3
After creating Initable3 ref
74
*///:~.class
只触发了类的加载,并不会触发类的初始化,但是Class.forName()
会触发类的加载- 类似Initable.staticFinal这样的final静态常量,并不会触发初始化。这很合理,因为这种静态变量在编译阶段就已经写死了,并不需要进行初始化
- 类似Initable.staticFinal2这样的final静态变量,会触发初始化
- 如果一个静态域field不是final的,那么获取他总是需要链接和初始化的步骤的
Generic class references
- 我们可以使用泛型来使Class类对象更具体,这样的话编译器就会在编译阶段检查Class类对象所代表的类。
1
2
3
4
5
6
7
8
9public class GenericClassReferences {
public static void main(String[] args) {
Class intClass = int.class;
Class<Integer> genericIntClass = int.class;
genericIntClass = Integer.class; // Same thing
intClass = double.class;
// genericIntClass = double.class; // Illegal
}
} ///:~ - 那么如果我们要放松泛型的限制该怎么做呢,可能有人会想到
Class<Number> genericNumberClass = int.class;
,但是这样是不对的,因为虽然Number类是Integer类的父类,但是Number Class不是Integer Class的父类。这个后面在泛型会深入讲解 - 真正可以放松限制的是使用泛型通配符
?
,它代表着任何类型。我们使用的时候应该使用Class<?>
而不是Class
1
2
3
4
5
6public class WildcardClassReferences {
public static void main(String[] args) {
Class<?> intClass = int.class;
intClass = double.class;
}
} ///:~ - 使用泛型的一个好处就是由于Class类对象的类型是确定的,当我们使用
newInstance()
的时候,返回的不会只是一个Object的对象,而是一个确定的类1
2
3
4
5
6
7
8
9
10
11
12
13public class GenericNewInstance {
public static void main(String[] args) {
Class<Test> testClass = Test.class;
try {
Test test = testClass.newInstance();
System.out.println(test);
} catch (Exception e) {
e.printStackTrace();
}
}
} ///:~
class Test {
}
Checking before a cast
x instanceof Y
可以用来进行类型检查,在进行类型转换的时候很有用,否则类型转换可能抛出ClassCastException
Rgistered factories
- 注册工厂是一种设计模式,需要进行一下学习
instanceof vs. Class equicalence
x instanceof Y
的意思是判断x对象是不是Y类型的实例,所以x既可以是Y类型的对象,也可以Y类型子类的对象。所以derivedClassObject instanceof BaseClass
返回true- 当我们比较Class类对象的时候,子类与父类的Class对象是不同的,所以
DrivedClass.class != BaseClass.class
Reflection: runtime class information
- 反射是指我们在编译阶段不知道类的具体信息,相关信息只有在runtime才会被得到,这个时候我们使用反射。反射与普通的RTTI没什么区别,只是一个是在编译阶段知道Class类对象的信息(.class字节码编译阶段就已存在),另一个是在runtime阶段才能够知道
java.lang.reflect
包提供了Field,Method和Constructor类。我们可以使用Custructor类去创建新的对象;使用get()和set()去读或者改变一个Field对象;使用invoke()去call一个Method对象
Dynamic proxies
- 动态代理是反射一个很好的应用例子: https://www.zhihu.com/question/20794107
Generics
https://www.cnblogs.com/wuqinglong/p/9456193.html
https://segmentfault.com/a/1190000020497160
Simple Generics(Generic Types)
https://docs.oracle.com/javase/tutorial/java/generics/types.html
Raw Type
A raw type is the name of a generic class or interface without any type arguments. For example, given the generic Box class:
1 | public class Box<T> { |
To create a parameterized type of BoxBox<Integer> intBox = new Box<>();
If the actual type argument is omitted, you create a raw type of BoxBox rawBox = new Box();
Therefore, Box is the raw type of the generic type Box
Raw types show up in legacy code because lots of API classes (such as the Collections classes) were not generic prior to JDK 5.0. When using raw types, you essentially get pre-generics behavior — a Box gives you Objects. For backward compatibility, assigning a parameterized type to its raw type is allowed:
1 | Box<String> stringBox = new Box<>(); |
The warning shows that raw types bypass generic type checks, deferring the catch of unsafe code to runtime. Therefore, you should avoid using raw types.
The Type Erasure section has more information on how the Java compiler uses raw types.
Raw Type的引用可以hold泛型对象,并且不会警告,这是为了兼容性;泛型的引用反之也可以hold Raw Type对象,但会报警告
由于有Raw Type的存在,泛型间的引用传递很危险,比如:
1 | List list = new ArrayList<String>();//no warning |
上面的例子只会报一个警告,但是是很危险的,因为我们把一个存有String的List对象被赋值给了一个List
Unchecked Error Messages
As mentioned previously, when mixing legacy code with generic code, you may encounter warning messages similar to the following:
Note: Example.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
This can happen when using an older API that operates on raw types, as shown in the following example:
1 | public class WarningDemo { |
The term “unchecked” means that the compiler does not have enough type information to perform all type checks necessary to ensure type safety. The “unchecked” warning is disabled, by default, though the compiler gives a hint. To see all “unchecked” warnings, recompile with -Xlint:unchecked.
Recompiling the previous example with -Xlint:unchecked reveals the following additional information:
WarningDemo.java:4: warning: [unchecked] unchecked conversion
found : Box
required: Box<java.lang.Integer>
bi = createBox();
^
1 warning
To completely disable unchecked warnings, use the -Xlint:-unchecked flag. The @SuppressWarnings(“unchecked”) annotation suppresses unchecked warnings. If you are unfamiliar with the @SuppressWarnings syntax, see Annotations.
Generic methods
前面我们已经展示了在类(或者接口,接口与类是类似的)上使用泛型的例子,我们也可以直接在方法上使用泛型,语法是:在返回值之前加上一个泛型参数列表(用<
和>
括起来)
1 | public class Util { |
Type Inference
当我们使用声明好的泛型方法的时候,我们并不需要像泛型类那样显式的声明我们所要使用的类,而是可以借助Java自带的类型推断来知道我们需要使用的类。类型推断可以根据我们传入泛型方法的参数的并集来判断我们使用的类
1 | class Test { |
这个例子中,我们两个泛型参数T分别被一个String和一个ArrayList表示,那么最终Java系统判断结果是String和ArrayList的并集也就是Serializable
Explicit type specification
- 虽然Java系统可以自动帮我们判断泛型参数最后会使用什么类,我们也可以显式的声明它,语法是在使用函数的
.
的后面加上想使用的类型:
`Serializable s = Test.pick(“d”, new ArrayList ());
Generics, Inheritance, and Subtypes
https://docs.oracle.com/javase/tutorial/java/generics/inheritance.html
a->b的意思是a是b的子类Integer->Number
,但是List<Number>
不是List<Integer>
的父类,两者没有任何关系!List<Number>
List<Integer>
-> Object
ArrayList<Integer>
-> List<Integer>
-> Collection<Integer>
The mystery of erasure
Java的泛型并不是在最开始就有的,而是在Java1.5才被加入的,所以Java泛型是由擦除实现的,也就是说关于泛型的信息在runtime是看不到的,所以
List<String>
和List<Integer>
本质上在runtime是一种类型:1
2
3
4
5
6
7
8
9
10
11import java.util.*;
public class ErasedTypeEquivalence {
public static void main(String[] args) {
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println(c1 == c2);
}
} /* Output:
true
*///:~擦除的作用效果是擦除到一个泛型的边界,如果一个泛型没有定义边界,那么它就会被擦除成
Object
类;如果一个泛型定义了边界<T extends Integer>
,那么就会擦除到它的边界也就是这里的Integer。1
2
3public class HasF {
public void f() { System.out.println("HasF.f()"); }
} ///:~1
2
3
4
5
6
7class Manipulator<T> {
private T obj;
public Manipulator(T x) { obj = x; }
// Error: cannot find symbol: method f():
public void manipulate() { obj.f(); }
}这里由于T没有定义边界,被擦除成了Object,那么它不认为自己有
f()
方法1
2
3
4
5class Manipulator2<T extends HasF> {
private T obj;
public Manipulator2(T x) { obj = x; }
public void manipulate() { obj.f(); }
} ///:~这里由于T定义了边界,被擦除成了HasF,那么它知道自己有
f()
方法
先检查,再编译
Q: 既然说类型变量会在编译的时候擦除掉,那为什么我们往 ArrayList 创建的对象中添加整数会报错呢?不是说泛型变量String会在编译的时候变为Object类型吗?为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢?
A: Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。
例如:
1 | public static void main(String[] args) { |
在上面的程序中,使用add方法添加一个整型,在IDE中,直接会报错,说明这就是在编译之前的检查,因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该允许任意引用类型添加的。可实际上却不是这样的,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。
那么,这个类型检查是针对谁的呢?我们先看看参数化类型和原始类型的兼容。
以 ArrayList举例子,以前的写法:ArrayList list = new ArrayList();
现在的写法:ArrayList<String> list = new ArrayList<String>();
如果是与以前的代码兼容,各种引用传值之间,必然会出现如下的情况:
ArrayList
ArrayList list2 = new ArrayList
不过在第一种情况,可以实现与完全使用泛型参数一样的效果,第二种则没有效果。
因为类型检查就是编译时完成的,new ArrayList()只是在内存中开辟了一个存储空间,可以存储任何类型对象,而真正设计类型检查的是它的引用,因为我们是使用它引用list1来调用它的方法,比如说调用add方法,所以list1引用能完成泛型类型的检查。而引用list2没有使用泛型,所以不行。
举例子:
1 | public class Test { |
通过上面的例子,我们可以明白,类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。
自动类型转换
因为类型擦除的问题,所以所有的泛型类型变量最后都会被替换为原始类型。
既然都被替换为原始类型,那么为什么我们在获取的时候,不需要进行强制类型转换呢?
看下ArrayList.get()方法:
1 | public E get(int index) { |
可以看到,在return之前,会根据泛型变量进行强转。假设泛型类型变量为Date,虽然泛型信息会被擦除掉,但是会将(E) elementData[index],编译为(Date)elementData[index]。所以我们不用自己进行强转。当存取一个泛型域时也会自动插入强制类型转换。假设Pair类的value域是public的,那么表达式:Date date = pair.value;
也会自动地在结果字节码中插入强制类型转换
从以上两个小节我们得出结论:Java泛型的擦除使得代码在runtime是没有泛型信息的,泛型只作用于编译期间。编译期间,程序根据泛型信息检查输入是否符合标准,不符合会报出错误;同时根据泛型信息将输出自动进行类型转换
Compensating for erasure
- 由于擦除存在的原因,任何与runtime类型的操作泛型都不可以使用
1
2
3
4
5
6
7
8
9public class Erased<T> {
private final int SIZE = 100;
public static void f(Object arg) {
if(arg instanceof T) {} // Error
T var = new T(); // Error
T[] array = new T[SIZE]; // Error
T[] array = (T)new Object[SIZE]; // Unchecked warning
}
} ///:~
Creating instances of types
- 既然我们不能进行
new T()
的操作,解决的办法之一是使用反射:从上面的例子可以看出来,确实是可以使用反射来创建泛型类的实例,但是Integer的实例创建失败了,原因是Integer类没有默认的构造器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
32import static net.mindview.util.Print.*;
class ClassAsFactory<T> {
T x;
public ClassAsFactory(Class<T> kind) {
try {
x = kind.newInstance();
} catch(Exception e) {
throw new RuntimeException(e);
}
}
}
class Employee {}
public class InstantiateGenericType {
public static void main(String[] args) {
ClassAsFactory<Employee> fe =
new ClassAsFactory<Employee>(Employee.class);
print("ClassAsFactory<Employee> succeeded");
try {
ClassAsFactory<Integer> fi =
new ClassAsFactory<Integer>(Integer.class);
} catch(Exception e) {
print("ClassAsFactory<Integer> failed");
}
}
} /* Output:
ClassAsFactory<Employee> succeeded
ClassAsFactory<Integer> failed
*///:~ - 还可以使用工厂设计模式(Factory Pattern)或者模板设计模式(Template Method)
Wildcards
Upper Bounded Wildcards
List<? extends Number>
的意思是List可以包含任何Number或者Number的子类,所以以下几种表达都是合理的
List<? extends Number> list= new LinkedList<Number>
List<? extends Number> list= new LinkedList<Integer>
List<? extends Number> list= new LinkedList<Double>
所以这就导致了List<? extends Number>
是不可写的(不能使用add),因为如果写了一个Integer,但实际存储的是new LinkedList<Double>
,就会出现错误但是
List<? extends Number>
是可读的(可以使用get),因为我们知道读出来的一定是Number或者Number的子类
Lower Bounded Wildcards
List<? super Integer>
的意思是List可以包含任何Integer或者Integer的父类,所以以下几种表达都是合理的
List<? super Integer> list= new LinkedList<Integer>
List<? super Integer> list= new LinkedList<Number>
List<? super Integer> list= new LinkedList<Object>
所以这就导致了
List<? super Integer>
只能读出Object类型
List<? super Integer>
是可写的,但是只可以写Integer或者Integer的子类所以总结来说,判断能否写是根据所有的可能性取一个交集,如果交集不存在就不能写;判断能否读是根据所有的可能性取一个并集,最差也能是Object,以为Object是所有类的父类
Unbounded Wildcards
- 根据上面的总结,
List<?>
不可写,只能读出Object - There are two scenarios where an unbounded wildcard is a useful approach:
If you are writing a method that can be implemented using functionality provided in the Object class. 对应第一条
When the code is using methods in the generic class that don’t depend on the type parameter. For example, List.size or List.clear. In fact, Class<?> is so often used because most of the methods in Classdo not depend on T. 因为第一条的限制,所以只能做与泛型读写无关的操作
Wildcards and Subtyping
https://docs.oracle.com/javase/tutorial/java/generics/subtyping.htmlList<Integer>
-> List<?>
List<String>
-> List<?>
,但是List<Integer>
和List<String>
之间没有继承关系List<Integer>
-> List<? extends Integer>
-> List<? extends Number>
-> List<?>
List<Number>
-> List<? super Number>
-> List<? super Integer>
-> List<?>
如何判断泛型之间的继承关系?
通过比较泛型所能代表的类,如果一个泛型所能代表的类是另一个所能代表的类的子集,那他就是另一个类的子类
比如? extends Integer
可以代表的类有Integer以及所有继承Integer的类;? extends Number
可以代表的类有Number以及所有继承Number的类,明显后面是前面的父集
同理,? super Intege
所能代表的类是? super Number
的父集,所以List<? super Integer>
是List<? super Number>
的父类
1 | List<?> list1 = new LinkedList<String>(); |
Java泛型通配符 ? 与 T 的区别
https://segmentfault.com/a/1190000020497160
这里解释的最好的一句就是?是一个实参,而T只是一个形参(占位符),也就是说T只能存在于泛型的编写过程中,最终在使用的时候都会被一个实参替换掉,这个实参甚至有可能是?
1 | import java.util.*; |
也可以使用? extends String
作为实参
1 | import java.util.*; |
Self-bounded types
泛型类的自限定
- 自限定的意思是一个类的对象只能与另一个这个类的对象进行作用。
- 泛型类的自限定是非常常见的,比如Integer类的声明
public final class Integer implements Comparable<Integer>
。它的含义是:Integer类实现了一个’使用Integer类的Comparable接口’。这样的好处是什么呢?Integer对象在使用Comparable的compareTo方法的时候只能和另一个Integer对象作用,也就不可能出现5.compareTo("6")
的情况出现。这就是自限定的作用:强制Integer在使用compareTo的方法时使用另一个Integer
泛型声明的完全自限定
在上面Comparable的例子中,Comparable类本身的定义是没有任何限制的,就是
public interface Comparable<T>
,我们使用Comparable<Integer>
的原因就是为了达到自限定,限定compareTo方法只能使用Integer。但是如果我们假设编写Java的人不小心把Integer类定义为public final class Integer implements Comparable<String>
,那么这个时候我们compareTo方法只能使用String了,compareTo方法变得毫无意义,当然这个时候compareTo方法也就不叫自限定了。那么能不能把限制进一步,使得我们只要使用Comparable,就必须做到自限定,而不会出现public final class Integer implements Comparable<String>
这种错误了呢?如果我们把Comparable定义成下面的形式,就做到了凡是实现Comparable的类,必须是自限定的,上面
public final class Integer implements Comparable<String>
的情况就会报错了。1
class Comparable<T extends Comparable<T>>
那么问题来了:为什么Java编写人员不把Comparable定义成上面这样的完全自限定呢?原因很简单,为了给予开发人员更多的灵活性。
那么有没有哪个Java类使用了上面这种完全自限定的定义呢?Enum就是一个很好的例子public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable { }
。对于所有我们定义的Enum类,我们肯定都希望只能与相同的Enum类进行作用,肯定不希望月与年可以进行比较。
I/O
The File class
File类的名字虽然是File,但其实传入的参数是文件路径。所以它可以表示一个真正的文件,也可能只是表示一个目录
File对象既可以表示文件,也可以表示目录。特别要注意的是,构造一个File对象,即使传入的文件或目录不存在,代码也不会出错,因为构造一个File对象,并不会导致任何磁盘操作。只有当我们调用File对象的某些方法的时候,才真正进行磁盘操作。
例如,调用isFile()
,判断该File对象是否是一个已存在的文件,调用isDirectory()
,判断该File对象是否是一个已存在的目录:如果我们确认一个File对象时目录,那么我们可以使用
list()
方法来列出这个目录下所有的File对象;也可以使用list(FileNameFilter)
来筛选想要返回的File对象。FileNameFilter是一个接口,所以我们很自然的可以想到用匿名类来构造实现。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//: io/DirList3.java
// Building the anonymous inner class "in-place."
// {Args: "D.*\.java"}
import java.util.regex.*;
import java.io.*;
import java.util.*;
public class DirList3 {
public static void main(final String[] args) {
File path = new File(".");
String[] list;
if(args.length == 0)
list = path.list();
else
list = path.list(new FilenameFilter() {
private Pattern pattern = Pattern.compile(args[0]);
public boolean accept(File dir, String name) {
return pattern.matcher(name).matches();
}
});
Arrays.sort(list, String.CASE_INSENSITIVE_ORDER);
for(String dirItem : list)
System.out.println(dirItem);
}
} /* Output:
DirectoryDemo.java
DirList.java
DirList2.java
DirList3.java
*///:~注意到这里我们使用了内部类外面的变量args,所以需要把它定义成final。
File类不止可以判断是否是文件还是目录,我们还可以使用它创建或者删除文件,查看文件的属性等。
InputStream/OutputStream
InputStream
InputStream就是Java标准库提供的最基本的输入流。它位于java.io这个包里。java.io包提供了所有同步IO的功能。
要特别注意的一点是,InputStream并不是一个接口,而是一个抽象类,它是所有输入流的超类。这个抽象类定义的一个最重要的方法就是int read(),签名如下:
public abstract int read() throws IOException;
。这个方法会读取输入流的下一个字节,并返回字节表示的int值(0~255)。如果已读到末尾,返回-1表示不能继续读取了。FileInputStream是InputStream的一个子类。顾名思义,FileInputStream就是从文件流中读取数据。下面的代码演示了如何完整地读取一个FileInputStream的所有字节:
1
2
3
4
5
6
7
8
9
10
11
12public void readFile() throws IOException {
// 创建一个FileInputStream对象:
InputStream input = new FileInputStream("src/readme.txt");
for (;;) {
int n = input.read(); // 反复调用read()方法,直到返回-1
if (n == -1) {
break;
}
System.out.println(n); // 打印byte的值
}
input.close(); // 关闭流
}在读取流的时候,一次读取一个字节并不是最高效的方法。很多流支持一次性读取多个字节到缓冲区,对于文件和网络流来说,利用缓冲区一次性读取多个字节效率往往要高很多。InputStream提供了两个重载方法来支持读取多个字节:
int read(byte[] b):读取若干字节并填充到byte[]数组,返回读取的字节数
int read(byte[] b, int off, int len):指定byte[]数组的偏移量和最大填充数利用上述方法一次读取多个字节时,需要先定义一个byte[]数组作为缓冲区,read()方法会尽可能多地读取字节到缓冲区, 但不会超过缓冲区的大小。read()方法的返回值不再是字节的int值,而是返回实际读取了多少个字节。如果返回-1,表示没有更多的数据了。
利用缓冲区一次读取多个字节的代码如下:
1
2
3
4
5
6
7
8
9
10public void readFile() throws IOException {
try (InputStream input = new FileInputStream("src/readme.txt")) {
// 定义1000个字节大小的缓冲区:
byte[] buffer = new byte[1000];
int n;
while ((n = input.read(buffer)) != -1) { // 读取到缓冲区
System.out.println("read " + n + " bytes.");
}
}
}
FilterInputStream
Java的IO标准库提供的InputStream根据来源可以包括:
FileInputStream:从文件读取数据,是最终数据源;
ServletInputStream:从HTTP请求读取数据,是最终数据源;
Socket.getInputStream():从TCP连接读取数据,是最终数据源;
等等如果我们要给FileInputStream添加缓冲功能,则可以从FileInputStream派生一个类:
BufferedFileInputStream extends FileInputStream
如果要给FileInputStream添加计算签名的功能,类似的,也可以从FileInputStream派生一个类:
DigestFileInputStream extends FileInputStream
如果要给FileInputStream添加加密/解密功能,还是可以从FileInputStream派生一个类:
CipherFileInputStream extends FileInputStream
如果要给FileInputStream添加缓冲和签名的功能,那么我们还需要派生
BufferedDigestFileInputStream
。如果要给FileInputStream添加缓冲和加解密的功能,则需要派生BufferedCipherFileInputStream
。我们发现,给FileInputStream添加3种功能,至少需要3个子类。这3种功能的组合,又需要更多的子类。
为了解决这种问题,Java采用装饰器(decorator)模式设计输入输出流。比如输入流可以分为两大类:
一类是直接提供数据的基础InputStream,例如:
FileInputStream
ByteArrayInputStream
ServletInputStream
等等一类是提供额外附加功能的InputStream,他们都继承自抽象类FilterInputStream。例如:
BufferedInputStream
DigestInputStream
CipherInputStream
等等当我们想要真正使用输入流的时候,需要先定义直接提供数据的基础的InputStream,再在外面套上各种FilterInputStream,但无论如何,最终得出的都是一个InputStream,他都有
int read()
方法:1
2
3InputStream file = new FileInputStream("test.gz");
InputStream buffered = new BufferedInputStream(file);
InputStream gzip = new GZIPInputStream(buffered);我们可以边写自己需要的FilterInputStream:
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
32
33
34
35
36
37
38
39
40public class Main {
public static void main(String[] args) throws IOException {
byte[] data = "hello, world!".getBytes("UTF-8");
try (CountInputStream input = new CountInputStream(new ByteArrayInputStream(data))) {
int n;
while ((n = input.read()) != -1) {
System.out.println((char)n);
}
System.out.println("Total read " + input.getBytesRead() + " bytes");
}
}
}
class CountInputStream extends FilterInputStream {
private int count = 0;
CountInputStream(InputStream in) {
super(in);
}
public int getBytesRead() {
return this.count;
}
public int read() throws IOException {
int n = in.read();
if (n != -1) {
this.count ++;
}
return n;
}
public int read(byte[] b, int off, int len) throws IOException {
int n = in.read(b, off, len);
if (n != -1) {
this.count += n;
}
return n;
}
}
Reader/Writer
上一节我们提到的InputStream和OutputStream,读入和写出的都是字节流,如果我们读取或者写入的是文本文件,那么我们还需要在进行编解码。为了解决这一痛点,Java引入了新的Reader和Writer类。本质上Reader/Writer与InputStream/OutputStream没有区别,只是Reader/Writer会根据操作系统默认的编解码配置进行编解码,所以操作的是字符流.
Java内部将char存成BMP unicode,但是由于char只有两个字节,所以对于两个字节以外的NON-BMP unicode,是无法用char表示的。如果我们不想要使用系统自带的编解码,比如有的时候中文使用默认编解码就会乱码,可以如下定义:
Reader reader = new FileReader("src/readme.txt", StandardCharsets.UTF_8);
。关于Unicode和UTF-8(http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html)Reader和InputStream有什么关系?
除了特殊的CharArrayReader和StringReader,普通的Reader实际上是基于InputStream构造的,因为Reader需要从InputStream中读入字节流(byte),然后,根据编码设置,再转换为char就可以实现字符流。如果我们查看FileReader的源码,它在内部实际上持有一个FileInputStream。既然Reader本质上是一个基于InputStream的byte到char的转换器,那么,如果我们已经有一个InputStream,想把它转换为Reader,是完全可行的。InputStreamReader就是这样一个转换器,它可以把任何InputStream转换为Reader。示例代码如下:
1
2
3
4// 持有InputStream:
InputStream input = new FileInputStream("src/readme.txt");
// 变换为Reader:
Reader reader = new InputStreamReader(input, "UTF-8");
Standard I/O
PrintStream是一种FilterInputStream,在OutputStream的接口上,额外提供了一些写入各种数据类型的方法:
写入int:print(int)
写入boolean:print(boolean)
写入String:print(String)
写入Object:print(Object),实际上相当于print(object.toString())System.out是系统自带的一个PrintStream,用于标准输出
与System.out不同,System.in只是一个InputStream,所以如果要使用标准输入读入,我们还需要对System.in进行包装
1
2
3
4
5
6
7
8
9
10
11
12
13import java.io.*;
public class Echo {
public static void main(String[] args)
throws IOException {
BufferedReader stdin = new BufferedReader(
new InputStreamReader(System.in));
String s;
while((s = stdin.readLine()) != null && s.length()!= 0)
System.out.println(s);
// An empty line or Ctrl-Z terminates the program
}
} ///:~
New I/O
JDK1.4介绍了java.nio
包,它的目的是更快速的进行I/O读写。具体细节这里不赘述
Object Serialization
- 序列化是指把一个Java对象变成二进制内容,本质上就是一个byte[]数组。
为什么要把Java对象序列化呢?因为序列化后可以把byte[]
保存到文件中,或者把byte[]
通过网络传输到远程,这样,就相当于把Java对象存储到文件或者通过网络传输出去了。
有序列化,就有反序列化,即把一个二进制内容(也就是byte[]
数组)变回Java对象。有了反序列化,保存到文件中的byte[]
数组又可以“变回”Java对象,或者从网络上读取byte[]
并把它“变回”Java对象。 - 实现了Serilizable接口的类都可以进行Java序列化与反序列化,类似Serializable这样的空接口被称为”标记接口”(Marker Interface)
1
2
3
4
5
6
7
8
9
10
11
12
13
14public class Main {
public static void main(String[] args) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
try (ObjectOutputStream output = new ObjectOutputStream(buffer)) {
// 写入int:
output.writeInt(12345);
// 写入String:
output.writeUTF("Hello");
// 写入Object:
output.writeObject(Double.valueOf(123.456));
}
System.out.println(Arrays.toString(buffer.toByteArray()));
}
} - 序列化的本质就是将Java code转成字节码,反序列化的时候再转译回来,所以反序列化得到的对象不会调用构造函数
- 由transient关键字声明的field不会被序列化,比如我们有一个password的field,我们并不想将其序列化并保存,这个时候可以将其声明为
private transient String password;
- 如果我们想要控制序列化与反序列化,比如调用构造函数,那么可以实现
Externalizable
接口 - Java的序列化机制仅适用于Java,如果需要与其它语言交换数据,必须使用通用的序列化方法,例如JSON
Enum
Basic enum features
1 | enum Shrubbery { GROUND, CRAWLING, HANGING } |
EnumClass.values()
返回一个枚举变量的数据,顺序为声明枚举的顺序ordinal()
按照枚举的顺序从0开始返回intname()
返回枚举的名字
Adding methods to an enum
- 除了无法继承一个enum之外(因为enum被编译后会变成
Class Example extends Enum<Example>
,已经继承过,不能再继承了),我们可以将enum当成普通的Java类 - 如果我们要为enum添加fields或者methods,那么enum的instances必须在类的开头声明,且最后需要加上
;
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public enum OzWitch {
// Instances must be defined first, before methods:
WEST("Miss Gulch, aka the Wicked Witch of the West"),
NORTH("Glinda, the Good Witch of the North"),
EAST("Wicked Witch of the East, wearer of the Ruby " +
"Slippers, crushed by Dorothy's house"),
SOUTH("Good by inference, but missing");
private String description;
// Constructor must be package or private access:
private OzWitch(String description) {
this.description = description;
}
public String getDescription() { return description; }
public static void main(String[] args) {
for(OzWitch witch : OzWitch.values())
print(witch + ": " + witch.getDescription());
}
} /* Output:
WEST: Miss Gulch, aka the Wicked Witch of the West
NORTH: Glinda, the Good Witch of the North
EAST: Wicked Witch of the East, wearer of the Ruby Slippers, crushed by Dorothy's house
SOUTH: Good by inference, but missing
*///:~ - Enum的方法也很普通类的方法一样可以override
The mystery of values()
- 如果我们查看Enum的Java文档,会发现并没有values()这个方法,原因是
values()
是一个由编译器在编译期间加入的静态方法。编译器还会加入一个valueOf()
方法(这个valueOf和Enum类自带的不一样,一个只有一个参数,一个有两个参数) - 由于
values()
是在编译期间加入的,所以说如果我们直接使用Enum类,是无法使用values()
的,这个时候可以使用反射Enum en = OzWitch.WEST.getClass().getEnumConstants()
Implements, not inherits
- Enum虽然不能继承父类,但是却可以实现接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25import java.util.*;
import net.mindview.util.*;
enum CartoonCharacter
implements Generator<CartoonCharacter> {
SLAPPY, SPANKY, PUNCHY, SILLY, BOUNCY, NUTTY, BOB;
private Random rand = new Random(47);
public CartoonCharacter next() {
return values()[rand.nextInt(values().length)];
}
}
public class EnumImplementation {
public static <T> void printNext(Generator<T> rg) {
System.out.print(rg.next() + ", ");
}
public static void main(String[] args) {
// Choose any instance:
CartoonCharacter cc = CartoonCharacter.BOB;
for(int i = 0; i < 10; i++)
printNext(cc);
}
} /* Output:
BOB, PUNCHY, BOB, SPANKY, NUTTY, PUNCHY, SLAPPY, NUTTY, NUTTY, SLAPPY,
*///:~
EnumSet and EnumMap
使用Enum实现的Set和Map
Constant-specfic methods
- Enum有一个特殊的功能,可以创建一个abstract method并且为每个Enum Instance进行不一样的实现 这让每个Enum Instance看起来像是一个独立的类,但要记住:这只是一种特殊的用法,他们并不能被当成独立的类使用,ConstantSpecificMethod才是一个类
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
26import java.util.*;
import java.text.*;
public enum ConstantSpecificMethod {
DATE_TIME {
String getInfo() {
return
DateFormat.getDateInstance().format(new Date());
}
},
CLASSPATH {
String getInfo() {
return System.getenv("CLASSPATH");
}
},
VERSION {
String getInfo() {
return System.getProperty("java.version");
}
};
abstract String getInfo();
public static void main(String[] args) {
for(ConstantSpecificMethod csm : values())
System.out.println(csm.getInfo());
}
}
Annotation
Basic syntax
Defining annotations
1 | import java.lang.annotation.*; |
- 注解的定义用@interface,我们使用元注解(例子中的@Target和@Retention)来声明注解的使用方法。@Target代表了我们的注解应该用在method还是field上;@Retention代表了注解存在于RUNTIME,SOURCE还是CLASS中。
- 注解中可以声明元素(elements,注解里我们不叫fields),用来表明注解中拥有的值。这些元素在处理注解的时候会被用到。注解的元素和类的方法很相似,区别是元素没有大括号以及元素可以有默认值。
Meta-annotations
- @Target 注解能被用在什么地方:CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE(Class, interface or enum)
- @Retention 注解信息会保存多久:SOURCE(Annotations are discarded by the compiler), CLASS(Annotations are available in the class file by the compiler but can be discarded by the JVM), RUNTIME(Annotations are retained by hte JVM at run time, os they may be read reflectively)
- @Documented 把注解保存到javadocs
Writing annotation processors
- 没有注解处理器,定义好的注解没有任何意义,编译器不会对其进行任何操作
- 我们可以使用反射来处理注解:
1
2
3
4
5
6
7
8
9
10
11
12
13
14import java.lang.reflect.*;
import java.util.*;
public class UseCaseTracker {
public static void trackUseCases(Class<?> cl) {
for(Method m : cl.getDeclaredMethods()) {
UseCase uc = m.getAnnotation(UseCase.class);
if(uc != null) {
System.out.println("Found Use Case:" + uc.id() +
" " + uc.description());
}
}
}
}
Annotation elements
- 注解元素允许的类型: All primitives, String, Class, Enums, Annotations, Arrays of any of the above。因此我们知道,注解元素不能使用封装类,但是可以使用nested annotation
Default value constrainsts
- 注解元素的默认值必须是确定的,所以对于空的String我们不能使用null,而应该使用
""
Nested annotation
1 | import java.lang.annotation.*; |
1 | import java.lang.annotation.*; |
如果注解中定义了value元素,那么使用的时候可以不用写key-value pair,比如可以直接使用
@SQLString(30)
,那么编译器默认这里的30是赋给value元素的,其他元素的使用默认值SQLString中的nested annotaion Constraints的默认值与@Constraints相同,我们也可以改变它的默认值
1
2
3public Uniqueness {
Constraints constraints() default @Constraints(unique=true);
}如何使用nested annotation:
@SQLString(value = 30, constraints = @Constraints(primaryKey = true))
如何处理nested annotation:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import java.lang.reflect.*;
import java.util.*;
public class SQLStringTracker {
public static void trackSQLString(Class<?> cl) {
for(Field f : cl.getDeclaredFields()) {
SQLString sqlString = f.getAnnotation(SQLString.class);
if(sqlString != null) {
Constraints con = sqlString.constraints();
System.out.println("Found sql string:" + sqlString.value() +
" " + con.primaryKey());
}
}
}
}
Using apt to process annotations
除了使用反射(RUNTIME)处理annotations, 还有别的方式处理annotation,比如在SOURCE CODE
或者CLASS阶段进行处理