学习Java编程思想的笔记。
内部类
java的内部类并不只是类似于C++的命名空间的包裹,而是一种和外部类进行组合的设计模式。在内部类中可以访问外围对象的所有成员,而不需要任何特殊条件,就像自己的成员变量一样。所以内部类的对象创建必须依托于一个外部类构建的对象。一种方式是在外部类的接口中提供生成内部类的对象(this被自动的包裹到内部类中),一种是通过外部类对象构造内部类对象:
public class Outer { |
所以从设计上说,内部类是对外部类逻辑的重新划分。比如设计类的时候,发现部分逻辑可以提取出来自成单元,就可以通过内部类的方式来组织。比如Iterator经常这么搞:
public class SomeCollection<T> { |
上面用法将测试代码委托到单独的内部嵌套类中进行,这样子发布代码时可以将Outer$Test.class
删除,而不会将测试代码发布出去。调用测试,执行java Outer$Test
即可。
enum
java中的enum非常有意思,声明enum的类会默认继承系统提供的Enum
基类,提供基本的操作枚举的方法。同时,enum中声明的常量会变成类似public static final的enum类变量:
enum Fruit { |
每一个定义的常量还对应了一个数值,使用APPLE.ordinal()
获取,通过接口Fruit.values()
得到当前所有常量对象。
enum有趣的的地方是可以定义常量对象的方法,很方便实现类似状态机的设计模式:
enum Handler { |
比如,上面的类中定义了一个抽象方法,而每一个Handler
中的常量都重新定义了具体的接口。配合容器EnumSet
和EnumMap
可以对状态进行配置。(内部使用bitset结构高效索引,只对EnumMap类型有效)。
注解
本来以为注解类似于python的修饰器,学习了一下发现根本不是一回事。java的注解并不会修改被修饰内容的运行逻辑,只是在被修饰物体上加入了标签信息。这些标签信息需要被额外的APT(Anonotation Processing Tool)组件所处理才可以hook生效运行效果。
定义一个注解类似于定义一个接口,只是使用关键字@interface
。同时还可以加入一些元注解(用来修饰注解的注解),常用的有 @Target
表示当前注解用在什么地方,而@Retention
表示注解的生命周期:
- SOURCE:只保留在源代码中,编译器编译时,直接丢弃这种注解,不记录在.class文件中。
- CLASS:编译器把注解记录在class文件中。当运行Java程序时,JVM中不可获取该注解信息,这是默认值。
- RUNTIME:编译器把注解记录在class文件中。当运行Java程序时,JVM可获取该注解信息,程序可以通过反射获取该注解的信息。
注解也可以有属性,而且可以在调用时候指定。如果注解只有一个属性叫value
,调用的时候可以不指定属性名而直接调用,这是一个默认的规则。
(ElementType.METHOD) |
很多类库使用注解来对代码逻辑进行hook,以便提供简单快捷的框架调用。java编程思想书中提到了一个简单的测试框架,使用RUNTIME级别的注解配合反射机制来动态的运行测试,并反映测试结果。大致执行代码如下:
void <T> process(Class<T> testClass) { |
参考资料:
容器
java的容器的用下面这张图可以完美的展示:
javaSE5中额外添加了:
Queue
接口,Queue
接口有集中实现类:LinkedList
,PriorityQueue
和各种版本的BlockingQueue
(ArrayBlockingQueue/LinkedBlockingQueue/PriorityBlockingQueue)ConcurrentMap
接口及其实现ConcurrentHashMap
- 实现List接口的
CopyOnWriteArrayList
和实现Set接口的CopyOnWriteArraySet
- 为了Enum特殊设计的
EnumSet
和EnumMap
继承体系中,其实只有四种接口容器:Map、List、Set和Queue,它们各自有对应的实现版本。除了Map,其余的实现体都是Collection类型,Map和Collection的主要区别在于管理的元素是一维的还是一个tuple。也因此,Map类型单独构造了一个独立于Collection外的继承体系,虽然两者提供的操作语义非常相似。
java的容器结构中,除了定义基本的接口,还定义了以Abstract为前缀的抽象类,这些类提供了结构的部分实现,如果需要自定义的容器,可以继承这些抽象类进行再定义处理。
使用Arrays.asList
接口可以给数组提供一个list的视图,但是注意,不可以增加或者删除元素,如果修改了list的内容,也会导致底层的数组元素的修改。
这篇文章对容器的api进行了更加详细的说明:参考
如果将按元素放入到Hash容器中,必须要同时实现equals
和hashcode
两个接口。hashcode
接口用来生成不唯一的对象标识,以便确定对象槽位。而equals
接口才是最终判断对象是否一样的接口。两者缺一不可,需要同时提供实现体。
异常
java的异常说明是一种自上而下强制执行异常的机制,在编译器就可以提供一定的异常检测。
一个方法可以声明将抛出特定的异常A,但实际上并不抛出这个异常。这样子做是提前给可能将来出现的异常占个坑,这样子调用端必须提前对该异常进行处理。
Throwable
是java的异常基类,继承体系中有两种类型:
Error
表示编译器和系统错误,一般不需要关心。Exception
表示可以被抛出的基本类型。
从Exception
中继承出的RuntimeExceptoin
表示运行时异常,比如NullPointerException
异常。这类异常不需要在异常说明中列出来,也被叫做『不受检查的异常』。
RuntimeException
异常如果一直传递到main函数,会导致程序退出,并在System.err
中打印异常堆栈信息,所以最好再某一个上层上进行统一处理。
继承体系中,如果基类接口定义了A,B两个异常,那么子类覆盖基类的接口只可以缩小异常范围,而不可以增加。比如只声明会抛出A异常,但是不可以声明抛出C异常。
作者在书中分析了java异常说明的优缺点:
- 异常设计的本意是将代码的调用和错误处理分开,这样子不必将错误处理代码散落在各处,可以统一处理。(如果有相同类型的错误处理逻辑,异常是非常方便的统一聚合点)
- 异常说明设计之初参考了C的异常说明机制,只是C的异常说明(throws语句)并不会在编译器强制要求调用者对异常进行处理,而只会在运行时检测抛出异常和异常说明是否一致。而java需要非常严格的编译器异常说明检测。
- java的强制异常说明导致需要写更多的错误处理代码,这违背了异常的本意,也会导致程序员为了偷工省事而直接吞噬异常。
所以作者推荐处理java异常的方法有:
- 只在你知道如何处理的情况下才捕获异常。如果不知道,就重新抛出。
- 将不知道如何处理的异常包裹成RunTimeException抛出。
class WrapCheckedException { |
上面的代码提供了一种思路,将受检测的异常包裹到不受检异常中,这样子异常不受检,也不会导致被无故吞噬。同时,利用异常调用链将原始异常保存起来,再可以处理异常的diff,取出不受检异常中的原始异常进行处理,或者在不知道如何处理的情况下,继续抛出。
反射
java的反射主要通过Object系统和对应的类型对象Class
来实现。Class对象中提供了当前对象的方法,字段,构造函数,继承关系,标记等信息,程序可以根据该信息来做一些动态hook。
获取Class对象有两种方法:
// 使用forName需要使用全限定类名,java会自动加载该类,并提供类对象(当然已经加载就不需要初始化类了,而是直接提供类对象) |
如果两个类型是继承关系,可以通过向上转型,使用父类类型来引用子类类型的对象。但如果将类型用到泛型参数中,不再继续符合对应的继承关系:
public class Derived extends Base {} |
动态代理机制比较有意思,可以生成一个对特定接口进行代理的对象,该对象可以调用接口的方法,但实际执行内容我们可以动态hook。比如下面的代码,为所有代码调用都加入了输出:
// 实际执行的对象 |
泛型
java的泛型机制基于类型擦除,和C++的完全不一样,这是为了兼容以前没有用泛型实现的类库代码。(个人理解)擦除机制是指在运行时将泛型类中的类型信息去除,所以泛型类中保存的类型实例其实都是Object引用。所以,java泛型的便利之处更多在于提供编译期类型检查,以及对外提供数据时的自动类型转换。
public class Erased<T> { |
可以这么理解:如果用来get数据,容器中不管放置的是什么类型,都需要规约到一个数据类型T,这个T就是所有类型的基类,所以用extends关键字来将类型向上擦除到基类T;如果用来put数据,容器中的类型就应该是基类的类型,至于具体是哪一种基类类型不重要,但一定是T的基类类型,才可以将T的数据放入。
关于mixin:在C++中,可以通过多重继承,或者是模板继承链的方式来实现mixin(比如template<class T> class SomeMixin : public T
)。而java中既不支持多继承,也不支持将泛型类型T做为父类型(因为类型擦除,运行时没有办法知道当前父类是啥了)。
java的mixin的一种实现方式是利用代理的方式来构造对象的组合:
class MixinProxy implements InvocationHandler { |
上面代码主要逻辑就是构造一个代理,这个代理中记录了各个接口类对应实现体的对象。在调用具体方法时,根据方法名找到具体实现体调用,实现了组合模式的mixin。但使用起来非常蛋疼:
- 需要定义各种interface还对应的实现体
- 调用时还需要各种转型,否则编译都无法通过。
在java8中可以在interface中定义默认方法,这给mixin机制提供了更好的实现(虽然也还是不如python,C++,ruby中的实现来的直接):
interface ContainerMixin { |
这样子只需要将状态相关的内容通过接口的方法委派给实现体即可,使用上比基于代理的方法要方便的多(至少实现了多个接口,调用方法时不需要进行转换)
参考文档:
数组
除非性能出现问题,否则优先使用容器而不是数组。数组是java最开始设计时候的产物,比包装类性能更好,但和java的OO体系有些地方不一致,所以使用上存在一些不便利。比如数组不可以在根据泛型参数进行构造,而容器是可以的。
Arrays
类中提供了操作数组的方法。如果需要输出数组,需要使用Arrays.toString
进行转换,否则输出的是数组对象本身。(个人理解应该是数组是原始类型,并没有toString
相关方法)
如果需要在泛型中使用数组,有两种方法:1)基于Collection.toArray
接口 2)传递数组类型的type进行构造。
// 手动传入一个T[]类型的数组,基于toArray进行转换 |
java6之后,推荐使用new MyClass[0]
的方式传递到toArray
方法中。参考: make-arraylist-toarray-return-more-specific-types
数组还有一个特性,构造的时候会自动初始化,这个对于刷算法题还是挺方便的。如果是int数组,自动初始化为0;如果是对象数组,自动初始化为null。
java的数组并不一定要指明所有维度(因为有length标识,内存结构上并不完全类似于C++那种一次性大内存块的模式的数组,还是有对象概念的),可以动态创建,且同一层次上维度并不需要相同:
int[][] array = new int[3][]; |