Java编程的逻辑
chapter 3 类的基础
3.3 代码的组织机制
包范围可见性
如果什么修饰符都不写,它的可见性范围就是同一个包内,同一个包内的其他类可以访问,而其他包内的类则不可以访问。
声明为protected不仅表明子类可以访问,还表明同一个包内的其他类可以访问,即使这些类不是子类也可以。
总结来说,可见性范围从小到大是:private < 默认(包) < protected < public。
chapter4 类的继承
4.1基本概念
多态:一种类型的变量可以引用多种实际类型对象。比如变量shape可以引用任何Shape子类类型的对象。
子类变量和方法与父类重名的情况下,可通过super强制访问父类的变量和方法。
子类对象可以赋值给父类引用变量,这叫多态;实际执行调用的是子类实现,这叫动态绑定。
关于可见性重写:重写方法时,一般并不会修改方法的可见性。但我们还是要说明一点,重写时,子类方法不能降低父类方法的可见性。不能降低是指,父类如果是public,则子类也必须是public,父类如果是protected,子类可以是protected,也可以是public,即子类可以升级父类方法的可见性但不能降低
为什么要这么规定呢?继承反映的是“is-a”的关系,即子类对象也属于父类,子类必须支持父类所有对外的行为,将可见性降低就会减少子类对外的行为,从而破坏“is-a”的关系,但子类可以增加父类的行为,所以提升可见性是没有问题的。
4.4 继承是把双刃剑
封装就是隐藏实现细节,提供简化接口。使用者只需要关注怎么用,而不需要关注内部是怎么实现的。实现细节可以随时修改,而不影响使用者。函数是封装,类也是封装。通过封装,才能在更高的层次上考虑和解决问题。可以说,封装是程序设计的第一原则,没有封装,代码之间会到处存在着实现细节的依赖,则构建和维护复杂的程序是难以想象的。
封装就是隐藏实现细节,提供简化接口。使用者只需要关注怎么用,而不需要关注内部是怎么实现的。实现细节可以随时修改,而不影响使用者。函数是封装,类也是封装。通过封装,才能在更高的层次上考虑和解决问题。可以说,封装是程序设计的第一原则,没有封装,代码之间会到处存在着实现细节的依赖,则构建和维护复杂的程序是难以想象的。
子类和父类之间是细节依赖,子类扩展父类,仅仅知道父类能做什么是不够的,还需要知道父类是怎么做的,而父类的实现细节也不能随意修改,否则可能影响子类。更具体的说,子类需要知道父类的可重写方法之间的依赖关系。
总结:对于子类而言,通过继承实现是没有安全保障的,因为父类修改内部实现细节,它的功能就可能会被破坏;而对于基类而言,让子类继承和重写方法,就可能丧失随意修改内部实现的自由。
4.4.3 继承没有反映 is-a 关系
继承关系是设计用来反映is-a关系的,子类是父类的一种,子类对象也属于父类,父类的属性和行为也适用于子类。就像橙子是水果一样,水果有的属性和行为,橙子也必然都有。
但现实中,设计完全符合is-a关系的继承关系是困难的。比如,绝大部分鸟都会飞,可能就想给鸟类增加一个方法fly()表示飞,但有一些鸟就不会飞,比如企鹅。
chapter 5 类的扩展
5.1接口的本质
5.1.6 使用接口替代继承
继承至少有两个好处:一个是复用代码;另一个是利用多态和动态绑定统一处理多种不同子类的对象。使用组合替代继承,可以复用代码,但不能统一处理。使用接口替代继承,针对接口编程,可以实现统一处理不同类型的对象,但接口没有代码实现,无法复用代码。将组合和接口结合起来替代继承,就既可以统一处理,又可以复用代码了。
5.2.2 为什么需要抽象类
引入抽象方法和抽象类,是Java提供的一种语法工具,对于一些类和方法,引导使用者正确使用它们,减少误用。使用抽象方法而非空方法体,子类就知道它必须要实现该方法,而不可能忽略,若忽略Java编译器会提示错误。使用抽象类,类的使用者创建对象的时候,就知道必须要使用某个具体子类,而不可能误用不完整的父类。无论是编写程序,还是平时做其他事情,每个人都可能会犯错,减少错误不能只依赖人的优秀素质,还需要一些机制,使得一个普通人都容易把事情做对,而难以把事情做错。抽象类就是Java提供的这样一种机制。
5.2.3 抽象类和接口
抽象类,相对于具体类,它用于表达抽象概念,虽然从语法上抽象类不是必需的,但它能使程序更为清晰,可以减少误用。抽象类和接口经常相互配合,接口定义能力,而抽象类提供默认实现,方便子类实现接口。
5.3.2 成员内部类
与静态内部类不同,成员内部类中不可以定义静态变量和方法(final变量例外,它等同于常量),下面介绍的方法内部类和匿名内部类也都不可以。Java为什么要有这个规定呢?可以这么理解,这些内部类是与外部实例相连的,不应独立使用,而静态变量和方法作为类型的属性和方法,一般是独立使用的,在内部类中意义不大,而如果内部类确实需要静态变量和方法,那么也可以挪到外部类中。
chapter6 异常
6.3异常处理
6.3.3 finally
finally语句有一个执行细节,如果在try或者catch语句内有return语句,则return语句在finally语句执行结束后才执行,但finally并不能改变返回值,我们来看下面的代码:
public static int test(){
int ret = 0;
try{
return ret;
}finally{
ret = 2;
}
}
这个函数的返回值是0,而不是2。实际执行过程是:在执行到try内的return ret;语句前,会先将返回值ret保存在一个临时变量中,然后才执行finally语句,最后try再返回那个临时变量,finally中对ret的修改不会被返回。如果在finally中也有return语句呢?try和catch内的return会丢失,实际会返回finally中的返回值。finally中有return不仅会覆盖try和catch内的返回值,还会掩盖try和catch内的异常,就像异常没有发生一样,比如:
public static int test(){
int ret = 0;
try{
int a = 5/0;
return ret;
}finally{
return 2;
}
}
finally中抛出了RuntimeException,则原异常ArithmeticException就丢失了。所以,一般而言,为避免混淆,应该避免在finally中使用return语句或者抛出异常,如果调用的其他代码可能抛出异常,则应该捕获异常并进行处理。
chapter 7 常用基础类
7.1 包装类
7.1.2共同点
hashCode和equals方法联系密切,对两个对象,如果equals方法返回true,则hashCode也必须一样。反之不要求,equal方法返回false时,hashCode可以一样,也可以不一样,但应该尽量不一样。hashCode的默认实现一般是将对象的内存地址转换为整数,子类如果重写了equals方法,也必须重写hashCode。之所以有这个规定,是因为Java API中很多类依赖于这个行为,尤其是容器中的一些类。
chapter8 泛型
泛型的好处:
- 更好的安全性
- 更好的可读性
/**
* <T extends Comparable<T>> 称为递归类型限制
* T表示一种数据类型,必须实现Comparable接口,
* 且必须可以与相同类型的元素进行比较。
*/
public static <T extends Comparable<T>> T max(T[] arr) {
T max = arr[0];
for (T t : arr) {
if (t.compareTo(max) > 0) {
max = t;
}
}
return max;
}
public class DynamicArray<E> {
private static final int DEFAULT_CAPACITY = 10;
private int size;
private Object[] elementData;
public DynamicArray() {
this.elementData = new Object[DEFAULT_CAPACITY];
}
private void ensureCapacity(int minCapacity) {
int oldCapacity = elementData.length;
if (oldCapacity >= minCapacity) {
return;
}
int newCapacity = oldCapacity * 2;
if (newCapacity < minCapacity) newCapacity = minCapacity;
elementData = Arrays.copyOf(elementData, newCapacity);
}
public void add(E e) {
ensureCapacity(size + 1);
elementData[size++] = e;
}
/**
* <T extends E> 用于定义类型参数,它申明了一个类型参数T,可放在泛型类定义中类名后面、泛型方法返回值前面
*/
public <T extends E> void addAll(DynamicArray<T> c) {
for (int i = 0; i < c.size; i++) {
add(c.get(i));
}
}
public void addAll0(DynamicArray<E> c) {
for (int i = 0; i < c.size; i++) {
add(c.get(i));
}
}
/**
* 这个方法没有定义类型参数,c的类型是DynamicArray<? extends E>,
* ?表示通配符,<? extends E>表示有限定通配符,匹配E或E的某个子类型,
* 具体什么子类型是未知的。
* </p>
* <? extends E>用于实例化类型参数,它用于实例化泛型变量中的类型参数,
* 只是这个具体类型是未知的,只知道它是E或者E的子类型
*/
public void addAll1(DynamicArray<? extends E> c) {
for (int i = 0; i < c.size; i++) {
add(c.get(i));
}
}
public E get(int index) {
return (E) elementData[index];
}
public int size() {
return size;
}
public E set(int index, E element) {
E oldValue = get(index);
elementData[index] = element;
return oldValue;
}
public static <T> int indexOf(T[] arr, T elm) {
for (int i = 0; i < arr.length; i++) {
if (arr[i].equals(elm)) {
return i;
}
}
return -1;
}
public static int indexOf(DynamicArray<?> arr, Object elm) {
for (int i = 0; i < arr.size; i++) {
if (arr.get(i).equals(elm)) {
return i;
}
}
return -1;
}
public static <T> int indexOf0(DynamicArray<T> arr, Object elm) {
for (int i = 0; i < arr.size; i++) {
if (arr.get(i).equals(elm)) {
return i;
}
}
return -1;
}
/**
* <T extends Comparable<T>> 称为递归类型限制
* T表示一种数据类型,必须实现Comparable接口,
* 且必须可以与相同类型的元素进行比较。
*/
public static <T extends Comparable<T>> T max(T[] arr) {
T max = arr[0];
for (T t : arr) {
if (t.compareTo(max) > 0) {
max = t;
}
}
return max;
}
/**
* 通配符有一个限制:只能读,不能写。如果能写就违背了java关于类型安全的承诺
*/
public static void swap(DynamicArray<?> arr, int i, int j) {
Object tmp = arr.get(i);
// 下面两行是错误的
// arr.set(i, arr.get(j));
// arr.set(j, tmp);
swapInternal(arr, i, j);
}
public static <T> void swapInternal(DynamicArray<T> arr, int i, int j) {
T tmp = arr.get(i);
arr.set(i, arr.get(j));
arr.set(j, tmp);
}
public static <D, S extends D> void copy(DynamicArray<D> dest, DynamicArray<S> src) {
for (int i = 0; i < src.size; i++) {
dest.add(src.get(i));
}
}
public static <D> void copy0(DynamicArray<D> dest, DynamicArray<? extends D> src) {
for (int i = 0; i < src.size; i++) {
dest.add(src.get(i));
}
}
}
关于通配符和类型参数的总结:
- 通配符形式都可以用类型参数的形式来替代,通配符能做的,用类型参数都能做。
- 通配符可以减少类型参数,形式上往往更为简单,可读性也更好,所以能用通配符就用通配符。
- 如果类型参数之间有依赖关系,或者返回值依赖类型参数,或者需要写操作,则只能用类型参数。
- 通配符形式和类型参数往往配合使用,比如上面的copy方法,定义必要的类型参数,使用通配符表达依赖,并接受更广泛的数据类型。
chapter 9 列表和队列
ArrayList的主要方法:
public boolean add(E e) //添加元素到末尾
public boolean isEmpty() //判断是否为空
public int size() //获取长度
public E get(int index) //访问指定位置的元素
public int indexOf(Object o) //查找元素, 如果找到,返回索引位置,否则返回-1
public int lastIndexOf(Object o) //从后往前找
public boolean contains(Object o) //是否包含指定元素,依据是equals方法的返回值
public E remove(int index) //删除指定位置的元素, 返回值为被删对象
//删除指定对象,只删除第一个相同的对象,返回值表示是否删除了元素
//如果o为null,则删除值为null的元素
public boolean remove(Object o)
public void clear() //删除所有元素
//在指定位置插入元素,index为0表示插入最前面,index为ArrayList的长度表示插到最后面
public void add(int index, E element)
public E set(int index, E element) //修改指定位置的元素内容
下面这段代码会抛出异常: java.util.ConcurrentModificationException
public void remove(ArrayList<Integer> list){
for(Integer a : list){
if(a<=100){
list.remove(a);
}
}
}
原因:发生了并发修改异常,为什么呢?因为迭代器内部会维护一些索引位置相关的数据,要求在迭代过程中,容器不能发生结构性变化,否则这些索引位置就失效了。所谓结构性变化就是添加、插入和删除元素,只是修改元素内容不算结构性变化。
RandomAccess
public interface RandomAccess {
}
这个接口没有定义任何代码,这有什么用呢?这种没有任何代码的接口再java中被称为标记接口,用于声明类的一种属性。
实现了RandomAccess接口的类表示可以随机访问,可随机访问就是具备类似数组那样的特性,数据在内存是连续存放的,根据索引值就可以直接定位到具体的元素,访问效率很高。下节我们会介绍LinkedList,它就不能随机访问。
chapter22 注解
在声明式编程风格中,程序都由三个组件组成:
-
声明的关键字和语法本身。
-
系统/框架/库,他们负责解释、执行声明式语句。
-
应用程序,使用声明式风格写程序。
声明式编程:SQL语言、编写网页CSS、正则表达式、函数式编程。
chapter 23 动态代理
切面:日志、性能监控、权限检查、数据库事务。
chapter24 类加载机制
负责加载类的类就是类加载器。
一个程序运行时,会创建一个Application ClassLoader,在程序中用到ClassLoader的地方,如果没有指定,一般用的都是这个ClassLoader,所以,这个ClassLoader也被称为系统类加载器(System ClassLoader)。