乐趣区

Java编程思想学习笔记

前言

前阵子刚看完《Java 编程思想》这本书,写篇文章来记录一下知识点。

一切都是对象

1. 对象存储到什么地方?
(1)寄存器
最快的存储区,数量有限。根据需求分配,无法直接控制
(2) 堆栈
位于通用 RAM(随机访问存储器)中。某些数据存储于堆栈中,特别是对象引用。速度仅次于寄存器
(3)
用于存放所有的 Java 对象。当执行 new 操作时,会自动在堆里进行存储分配
(4) 常量存储
通常直接存放在程序代码内部
(5) 非 RAM 存储
数据存活于程序之外,在程序没有运行时也可以存在。两个基本的例子是流对象和持久化

2. 基本类型变量 (int/double/long/…..) 直接存储值并置于堆栈中,因此更加高效。

3. 当创建一个数组对象时,实际上就是创建了一个引用数组,并且每个引用都被初始化为 null。

4. 当变量作为类的成员使用时,Java 才确保给定其默认值。然而此方法并不适用于局部变量(定义于方法内的变量,局部变量不会被初始化)

5.main 方法中的 args 用来存储命令行参数。

操作符

1. 如果对对象使用 a =b,那么 a 和 b 都指向原本只有 b 指向的那个对象。

2.== 和!= 比较的是对象的引用。

3. 基本类型比较内容是否相等直接使用 == 和!= 即可。

4.Object 类的 equals 方法默认使用 ”==” 比较。

5. 大多数 Java 类库都重写了 equals 方法,以便用来比较对象的内容,而非比较对象的引用。

6. 如果对 char、byte 或 short 类型数值移位处理,在进行移位之前会先被转换为 int 类型,并且得到的结果也是一个 int 类型的值。

初始化和清理

1. 在 Java 中,” 初始化 ” 和 ” 创建 ” 被捆绑在一起,两者不能分离。

2. 涉及基本类型的重载:
如果传入的数据类型 (实参) 小于方法中声明的形参,实际数据类型就会被提升。
char 型有所不同,如果无法找到恰好接受 char 类型参数的方法,就会把 char 直接提升至 int。
如果传入的实际参数较大,就会强制类型转换。

3. 如果已经定义了一个构造器,编译器就不会帮你自动创建默认构造器。构造器实际上是 static 方法,只不过该 static 声明是隐式的

4.this 关键字只能在方法内部调用,表示对 ” 调用方法的那个对象 ” 的引用。

5. 在构造器中可通过 this 调用另一个构造器。但却不能调用两个构造器。此外,必须将构造器调用置于最起始处,否则会报错。

6. 在 static 方法的内部不能调用非静态方法,反过来是可以的。

7. 垃圾回收器只知道回收那些经由 new 分配的内存。

8.native(本地方法)是一种在 Java 中调用非 Java 代码的方式。

9. 在类的内部,变量定义的先后顺序决定了初始化的顺序。
即使变量定义散布于方法定义之间,它们仍旧会在任何方法 (包括构造器) 被调用之前得到初始化。

10. 无论创建多少个对象,静态数据都只占一份存储区域。

11. 静态初始化只有在必要时刻进行。只有在 "第一个对象被创建" 或者 "第一次访问静态数据" 的时候,它们才会被初始化。此后静态对象不会再被初始化。

12. 初始化的顺序是先静态对象(如果它们还没被初始化过),而后是非静态对象

复用类

1. 每一个非基本的对象都有一个 toString 方法。

2.Java 会自动在子类的构造器中插入对父类构造器的调用。所有构造器都会显示或隐式地调用父类构造器

3. 构建过程是从父类 ” 向外 ” 扩散的,所以父类在子类构造器可以访问它之前就已经完成了初始化。

4. 调用父类构造器是你在子类构造器中要做的第一件事

5. 向上转型:可以理解为 ” 子类是父类的一种类型 ”。

6. 对于基本类型,final 使数值恒定不变。对于对象引用,final 使引用恒定不变(即无法再把它指向另一个对象)。

7.final 类:防止别人继承。
final 参数:你可以读参数,但却无法修改参数。(一般用来向匿名内部类传递数据)
final 方法:把方法锁定,以防任何继承类修改它的含义。

多态

1. 把对某个对象的引用视为对其父类型的引用的做法被称为向上转型。

2. 只有非 private 方法才能被覆盖。

3. 域和静态方法不是多态的。(当子类转型为父类引用时,任何域访问操作都将由编译器解析,所以不是多态的)

4. 调用构造器要遵循下面的顺序:
(1)调用父类构造器。
(2)按声明顺序调用成员的初始化方法。
(3)调用子类构造器。

5. 为什么在子类构造器里要先调用父类构造器???
答:在构造器内部,我们必须要确保要使用的成员都已经构建完毕。为确保这一目的,唯一的方法就是首先调用父类构造器。那么在进入子类构造器时,在父类中可供我们访问的成员都已得到初始化。

6. 继承与清理
(1) 当覆盖父类的 dispose()方法时,务必记住调用父类版本的 dispose()方法,否则父类的清理动作就不会发生。
(2)应该首先对子类进行清理,然后才是父类。这是因为子类的清理可能会调用父类的某些方法,因此不该过早地销毁它们。

7. 构造器内部的多态行为

 Class A{void draw(){print("A.draw()");}
 A(){draw();}
 }
 Class B extends A{void draw(){print("B.draw()");}
 }
 Class Test{public static void main(String[] args){new B();
   }
 } 
 输出:B.draw()

在 B 构造器中调用父类 A 构造器,在里面调用的是子类 B 覆盖后的 draw()方法。

因此,编写构造器时有一条有效的准则:”如果可以的话,避免在构造器内部调用其它方法“。

接口

1. 包含抽象方法的类叫做抽象类。如果一个类包含一个或多个抽象方法,该类必须被限定为抽象的。

2. 如果继承抽象类,那么必须为基类中所有的抽象方法提供方法定义。如果不这样做,那么子类就必须限定为抽象类。

3. 接口中的域隐式地是 static 和 final 的。

4. 接口中的方法默认是 public 的,因此实现接口中的方法时必须定义为 public 的。否则其访问权限就降低了,这是 Java 不允许的。

5. 通过继承来扩展接口
interface A{….}
interface B{….}
interface C extends A,B{…..}// 仅适用于接口继承
一般情况下,只可以将 extends 用于单一类,但是可以引用多个基类接口。

6. 嵌套在类中的接口可以是 private 的。
嵌套在另一个接口中的接口自动是 public,而不能声明为 private 的。

7. 当实现某个接口时,并不需要实现嵌套在其内部的任何接口。而且,private 接口不能在定义它的类之外被实现。

内部类

1. 当生成一个内部类的对象时,内部类对象隐式地保存了一个引用,指向创建它的外部类对象。(静态内部类除外)
内部类对象能够访问外部类对象的所有成员和方法(包括私有的)。

  1. .new 和.this
   class A{class B{}
   }
(1)创建内部类对象:A a=new A();
   A.B b=a.new B();
(2)生成外部类对象引用:class A{
       class B{public A getA(){return A.this;}
   }
}

3. 如果定义一个匿名内部类,并且希望它使用一个在其外部定义的对象,那么编译器会要求其参数引用是 final 的。

4. 当创建静态内部类的对象时:
(1)不需要其外部类对象
(2) 不能从静态内部类对象中访问非静态的外部类对象

5. 不管一个内部类被嵌套多少层,它都能够访问所有它嵌入的外部类的所有成员。

6. 类文件命名规则:外部类名字 +$+ 内部类名字
(1) 普通内部类:A$B
(2)匿名内部类(编译器会生成一个数字作为其标识符) A$1

类型信息

1. 如果某个对象出现在字符串表达式中 (涉及 ”+” 和字符串对象的表达式),toString() 方法会被自动调用,以生成该对象的 String。

2. 每当编写并且编译了一个新类,就会产生一个 Class 对象。更恰当地说,是被保存在一个同名的.class 文件中。

3.Java 程序在它开始运行之前并非完全被加载,其各个部分是在必需时才加载的。
类加载器首先检查这个类的 Class 对象是否已经加载。如果尚未加载,默认的类加载器就会根据类名查找.class 文件。

4.Class 对象仅在需要的时候才被加载,static 初始化块是在类加载时进行的。

5.Class.forName("A") 用来加载类 A 并获取 A.Class 对象。字符串必须使用全限定名,包括包名。

6.Class AClass=A.class 当使用.class 来创建 Class 对象时,不会自动地初始化该 Class 对象

7.static final int a=1;
static final int b=ClassInitialization.rand.nextInt(1000);
static int c=1;
如果一个 static final 值是编译期常量,就像 a 那样,那么这个值不需要对初始化类就可以被读取。
读取 b 和 c 则需要先对类进行初始化。

泛型

1. 擦除
(1) 可以声明 ArryaList.class,但不能声明 ArrayList<Integer>.class;

(2)

Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println(c1 == c2);// 返回 true

(3)List<String> 和 List<Integer> 在运行时事实上是相同的类型,两者都被擦除为 List 类型。

2. 擦除的补偿

public class czy<T>{public static void f(Object arg){(1)if (arg instanceof T){} // 错误
        (2)T t = new T(); // 错误
        (3)T[] array = new T[10]; // 错误
    }
}

(1)无法使用 instanceof 是因为其类型信息已经被 擦除 了。
(2)new T()失败,部分原因是因为擦除,另一部分原因是因为编译器不能验证 T 具有默认构造器。
除非引入类型标签

public boolean f(Class<T> kind,Object arg){t = kind.newInstance(); // 正确
    return kind.isInstance(arg); // 正确
}

3.<? extends T> 表示类型的上界,表示参数化类型可能是 T 或 T 的子类

4. 如下

public void f(List<? super Apple> apples){apples.add(new Apple()); // 正确
    apples.add(new GreenApple()); // 正确  GreenApple 继承自 Apple
    apples.add(new Fruit()); // 错误
}

向 apples 其中添加 Apple 或 Apple 的子类型是安全的。
因为 Apple 或者 GreenApple 肯定是 <? super Apple> 的子类,所以编译通过。

5.List<?> 看起来等价于 List<Object>,而 List 实际上也是 List<Object>。

6. 自动包装机制不能应用于数组。

7.public class Czy<W,T>{void f(List<W> v){}
    void f(List<T> v){}// 错误}

由于擦除的原因,重载方法会产生相同的类型签名。

数组

1. 你不能实例化具有参数化类型的数组:

 Czy<A>[] czy = new Czy<A>[10];——> 编译错误

擦除会移除参数类型信息,而数组必须知道它们所持有的确切类型,以强制保证类型安全。

2. 复制数组
System.arraycopy()→用它复制数组比用 for 循环快得多。
System.arraycopy() 不会执行自动包装和自动拆包,两个数组必须具有相同的确切类型。

3. 数组比较
Arrays 类提供了重载后的 equals() 方法,用来比较整个数组。
此方法针对所有基本类型和 Object 都做了重载。

4. 数组排序
(1) 实现 Comparable 接口,实现 compareTo()方法。
如果没有实现 Comparable 接口,调用 Arrays.sort()方法会抛出异常。因为 sort 方法需要把参数类型变为 Conparable
(2)创建一个实现了 Comparator 接口的类。

容器深入研究

1.Arrays.asList()会生成一个固定尺寸的 List,该 List 只支持那些不会改变数组大小的操作。
任何对底层数据结构的尺寸进行修改都会抛出一个异常。
应该把 Arrays.asList()的结果作为构造器参数传递给 Collection,这样就可以生成允许使用所有方法的普通容器。
比如:List<String> list = new ArrayList<>(Arrays.asList(a));

2.Collections.unmodifiableList()产生不可修改的容器。

3. 散列码是 ” 相对唯一 ” 的,用以代表对象的 int 值。

4. 如果不为你的键覆盖 hashcode()和 equals(),那么使用散列的数据结构 (HashSet,HashMap,LinkedHashSet,LinkedHashMap) 就无法正确处理你的键。

5.LinkedHashMap 在插入时比 HashMap 慢一点,因为它维护散列表数据结构的同时还要维护链表(以保持插入顺序)。
正是由于这个链表,使得其迭代速度更快。

6.HashMap 使用的默认负载因子是 0.75.

7.Java 容器采用快速报错 (fail-fast) 机制。
它会检查容器上的任何除了你的进程所进行的操作以外的所有变化,一旦它发现其它进程修改了容器,就会抛出异常。防止在你迭代容器的时候,其它线程在修改容器的值。

8.WeakHashMap 允许垃圾回收器自动清理键和值。

9.Iterable 接口被 foreach 用来在序列中移动。如果你创建了任何实现 Iterable 接口的类,都可以将它用于 foreach 语句中。
foreach 语句可以用于数组或其它任何 Iterable,但这并不意味着数组也是一个 Iterable。

10. 总结

  • 如果要进行大量的随机访问,就使用 ArrayList;如果要经常从表中间插入或删除元素,就应该使用 LinkedList。
  • 各种 Queue 以及栈的行为,由 LinkedList 提供支持。
  • HashMap 用来快速访问。
    TreeMap 使得“键”始终处于排序状态,所以没有 HashMap 快。
    LinkedHashMap 保持元素插入的顺序。
  • Set 不接受重复元素。
    HashSet 提供最快的查询速度。
    TreeSet 保持元素处于排序状态。
    LinkedHashSet 以插入顺序保存元素。
  • 新程序中不应该使用过时的 Vector、Hashtable 和 Stack。
退出移动版