17

前言

之前在校的时候简单读了一遍《Java编程思想》这本书,老实说真的是晦涩难懂,书中大量的代码看的我晕乎乎的,笔者这里对一些知识点做了笔记,如果有不对的请多多指教!

一切都是对象

  1. 对象存储到什么地方?
  • 寄存器——最快的存储区,数量有限。根据需求分配,无法直接控制
  • 堆栈——位于通用RAM(随机访问存储器)中。某些数据存储于堆栈中,特别是对象引用。速度仅次于寄存器
  • ——用于存放所有的Java对象。当执行new操作时,会自动在堆里进行存储分配
  • 常量存储——通常直接存放在程序代码内部
  • 非RAM存储——数据存活于程序之外,在程序没有运行时也可以存在。两个基本的例子是流对象和持久化
  1. 基本类型变量直接存储值并置于堆栈中,因此更加高效
  2. 当创建一个数组对象时,实际上就是创建了一个引用数组,并且每个引用都被初始化位null
  3. 当变量作为类的成员使用时,Java才确保给定其默认值。然而此方法并不适用于局部变量,定义于方法内的变量,局部变量不会被初始化

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

操作符

  1. 如果对对象使用a = b,那么a和b都指向原本只有b指向的那个对象。
  2. ==!=比较的是对象的引用。
  3. 基本类型比较内容是否相等直接使用==!=即可。
  4. Object类的equals方法默认使用"=="比较。
  5. 大多数Java类库都重写了equals方法,以便用来比较对象的内容,而非比较对象的引用。
  6. 如果对char、byteshort类型数值移位处理,在进行移位之前会先被转换为int类型,并且得到的结果也是一个int类型的值。

初始化和清理

  1. 在Java中,"初始化"和"创建"被捆绑在一起,两者不能分离。
  2. 涉及基本类型的重载:
    **如果传入的数据类型(实参)小于方法中声明的形参,实际数据类型就会被提升。
    char型有所不同,如果无法找到恰好接受char类型参数的方法,就会把char直接提升至int。
    如果传入的实际参数较大,就会强制类型转换**。
  3. 如果已经定义了一个构造器,编译器就不会帮你自动创建默认构造器
  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. 为什么在子类构造器里要先调用父类构造器???

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

  1. 继承与清理

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

  1. 构造器内部的多态行为
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. 接口中的域隐式地是staticfinal
  4. 接口中的方法默认是public的,因此实现接口中的方法时必须定义为public的。否则其访问权限就降低了,这是Java不允许的。
  5. 通过继承来扩展接口
 interface A{....}
 interface B{....}
 interface C extends A,B{.....}//仅适用于接口继承

一般情况下,只可以将extends用于单一类,但是可以引用多个基类接口。

  1. 嵌套在类中的接口可以是private的。
    嵌套在另一个接口中的接口自动是public,而不能声明为private的。
  2. 当实现某个接口时,并不需要实现嵌套在其内部的任何接口。而且,private接口不能在定义它的类之外被实现。

内部类

  1. 当生成一个内部类的对象时,内部类对象隐式地保存了一个引用,指向创建它的外部类对象。(静态内部类除外)
    内部类对象能够访问外部类对象的所有成员和方法(包括私有的)。
  2. .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;
           }
       }
   }
  1. 如果定义一个匿名内部类,并且希望它使用一个在其外部定义的对象,那么编译器会要求其参数引用是final的。
  2. 当创建静态内部类的对象时:
    (1)不需要其外部类对象
    (2)不能从静态内部类对象中访问非静态的外部类对象
  3. 不管一个内部类被嵌套多少层,它都能够访问所有它嵌入的外部类的所有成员
  4. 类文件命名规则:外部类名字+$+内部类名字
    (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值是编译期常量,就像a那样,那么这个值不需要对初始化类就可以被读取。读取b和c则需要先对类进行初始化。
static final int a=1;
static final int b=ClassInitialization.rand.nextInt(1000);
static int c=1;

泛型

  1. 擦除
  • 可以声明ArryaList.class,但不能声明ArrayList<Integer>.class;
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println(c1 == c2);//返回true
  • List<String>List<Integer>在运行时事实上是相同的类型,两者都被擦除为List类型。
  1. 擦除的补偿
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); //正确
}
  1. <? extends T>表示类型的上界,表示参数化类型可能是T或T的子类
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>的子类,所以编译通过。

  1. List<?>看起来等价于List<Object>,而List实际上也是List<Object>
  2. 自动包装机制不能应用于数组。
  3. 由于擦除的原因,重载方法会产生相同的类型签名。
public class Czy<W,T>{
    void f(List<W> v){}
    void f(List<T> v){}//错误
}

容器深入研究

  1. **Arrays.asList()会生成一个固定尺寸的List,该List只支持那些不会改变数组大小的操作。
    任何对底层数据结构的尺寸进行修改都会抛出一个异常**。应该把Arrays.asList()的结果作为构造器参数传递给Collection,这样就可以生成允许使用所有方法的普通容器。
    比如:
List<String> list = new ArrayList<>(Arrays.asList(a));
  1. Collections.unmodifiableList()产生不可修改的容器。
  2. 散列码是"相对唯一"的,用以代表对象的int值。
  3. 如果不为你的键覆盖hashcode()equals(),那么使用散列的数据结构(HashSet,HashMap,LinkedHashSet,LinkedHashMap)就无法正确处理你的键
  4. LinkedHashMap在插入时比HashMap慢一点,因为它维护散列表数据结构的同时还要维护链表(以保持插入顺序)。正是由于这个链表,使得其迭代速度更快。
  5. HashMap使用的默认负载因子是0.75.
  6. Java容器采用快速报错(fail-fast)机制。
    它会检查容器上的任何除了你的进程所进行的操作以外的所有变化,一旦它发现其它进程修改了容器,就会抛出异常。 防止在你迭代容器的时候,其它线程在修改容器的值。
  7. WeakHashMap允许垃圾回收器自动清理键和值。
  8. Iterable接口被foreach用来在序列中移动。如果你创建了任何实现Iterable接口的类,都可以将它用于foreach语句中。

foreach语句可以用于数组或其它任何Iterable,但这并不意味着数组也是一个Iterable。

  1. 总结

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

总结

《Java编程思想》这本书笔者目前读到容器这部分,现在觉得这本书不太适合新手入门,新手我更推荐去看《Java核心技术》,等以后腾出时间了再把后面的补上。


超大只乌龟
882 声望1.4k 粉丝

区区码农