Chapter 1 概述

Java跨平台

Java 既具有解释型语言的特征,也具有编译型语言的特征。

Java既不是编译型语言也不是解释型语言,还是静态语言。

Java语言经过编译器编译后生成与平台无关的字节码文件(*.class,需要由Java解释器来解释执行)

不同操作系统有不同的虚拟机,但提供一个相同的面向编译器的接口。

只要为不同平台实现了相应的虚拟机,编译之后的Java字节码就可以在该平台上运行

编译与运行Java源文件

举例如下:

java命令运行文件,紧跟于java文件中的类名,如java HelloWorld(注意,java命令针对于 class 文件,但是在输入命令时不加.class后缀)

main方法

签名:public static void main(String[] arg)

其中,publicstatic 前后顺序可互换,但 public 可以省略不写,static 必须要使用

另外,返回值只能是 void ,否则该方法不是程序的入口方法

若写成 Main 方法,Java解释器无法找到 main 方法

Java源文件

  • 一个Java文件只能有一个 public 类,且文件名必须与 public 类名一致

  • 注意,每个源文件最多只有一个包声明,必须位于 Java 源文件的第一行

  • Java程序中所有的关键字都是小写,无须任何大写字母

编程错误

  • 语法错误
  • 运行错误
  • 逻辑错误

Chapter 2

Java标识符

所有关键字都是由小写英文字母组成的

基本数据类型

  • truefalse 只能小写

  • 表达式类型的自动提升:一个算术表达式包含多个基本类型的值时,整个算术表达式的数据类型都将发生自动提升,规则如下:

    • 所有 byteshortchar 类型自动提升到 int
    • 自动提升到表达式中最高等级操作同样的类型
    1
    2
    3
    4
    5
    6
    7
    8
    9
    byte b = 40;
    char c = 'a';
    int i = 23;
    double d = 3.5;
    double res = b + c + i * d;
    //最高级操作数d是double,右边所有变量类型自动提升到double,结果自然是double

    short s = 5;
    s = s - 2; //编译错误。右式自动提升到int,赋值时需强转
  • 一个值赋给范围更小类型的,必须强制转换(然而,会有进度降低或溢出)

Scanner

从控制台中读取字符串:next() 读取以空白字符结束的字符串

1
2
Scanner sc = new Scanner(System.in);
String s = sc.next();

字符编码

四种等价赋值方式:

1
2
3
4
char c = 'a';
char c = '\u0061'; // '\uXXXX'中'XXXX'代表一个十六进制的整数
char c = 0x0061;
char c = 97;

字符型与数值型数据的转换

当一个整数转换成 char 型数据时,只使用整数的低十六位,其他部分忽略,如下:

1
char c = (char)0xAB0041; //取最后0x41,得到'A'

若操作数是一个数字或字符,char 自动转换为数字,如下:

1
2
3
4
5
char c = '2' + '3'; //'e'
int x = '2' + '3'; //101
int y = 2 + 'a'; //99

char c = "d"; //显然是错误的!!!

对于字符串:

  • 对于+运算符,若操作数之一不是字符串,则非字符串自动转换为字符串,注意下方坑点:

    1
    2
    3
    4
    5
    System.out.println("1+2="+1+2); //12
    System.out.println("1+2="+(1+2)); //3

    System.out.println("1" + 1); //11
    System.out.println('1' + 1); //50,'1'自动转换为数字

    由此,数字转换成字符串的方法:

    1
    String s = number + "";
  • 字符串转换成数字:

    1
    double val = Double.parseDouble("23.3");

Chapter 3+5 流程控制

switch 语句

  • 对于 switch(expr) 语句,其 expr 表达式类型应与 int 类型兼容:byteshortcharint ,又或者是字符串,又或者是枚举类型

    注意,long 类型以及浮点类型,均不能够自动地转换为 int 类型

  • 对于 case valueN 子句中,valueN 必须是常量

random 方法

0Math.random()<1.00 \le Math.random() < 1.0 故:

  • 返回[a,a+b)[a, a+b)a + Math.random() * b
  • 返回[10,20)[10, 20)(int)(10 + Math.random()*10)
  • 随机返回 0 或 1:(int)(Math.random()*10)%2

Chapter 6. 方法

方法签名

方法名+参数列表

方法的重载

方法的重载与方法返回值类型、修饰符等没有任何关系。

因此,被重载的方法必须具有不同的参数列表,不能基于不同的修饰符或返回值类型(否则编译错误)

关于方法的匹配,你

static修饰符

静态方法不能够直接调用非静态成员变量和非静态方法,经典错误如下:

方法体

  • 对于有返回值的方法,return 语句必需的,尽管代码逻辑正确,但编译器并不认为是正确的(编译错误)

  • 对变量初始化时,注意下面的坑点:

方法调用

一个方法结束运行会返回调用者,同时其活动记录中所有变量会从栈中移出(释放所有变量),定义在方法中的变量(包括引用类型)也结束其声明周期

方法中参数传递

  • 一个方法不能修改一个基本数据类型的参数
  • 一个方法可改变一个对象参数的状态
  • 一个方法不能够让对象参数引用一个新的对象

Chapter 7. 一维数组

数组的初始化

实际数组元素被存储在堆heap内存中,数组引用变量是引用类型变量,被存储在栈内存中

  • 静态初始化:开发人员显式地指定每个数组元素的初始值,系统决定长度

    1
    2
    3
    int[] arr = new int[]{1, 2, 3, 4, 5}; //注意不要漏了中括号
    int
    float[] f = {1.2f, 3, 4f, 5}; //注意坑点
  • 动态初始化:开发人员只指定数组长度,系统为数组分配初始值

    1
    2
    int[] mylist; //声明整形的数组
    mylist = new int[5]; //创建数组并将数组的引用赋值给mylist

    注意:不能同时使用静态初始化和动态初始化

数组的长度

注意普通数组的长度为:arr.length;而 String 的长度为:arr.length(),此外:"Hello".length()

数组的复制

  1. 循环语句遍历赋值

  2. 使用 System 类中的静态方法 arraycopy():内容相同,引用不同

    1
    2
    3
    int[] list = {1, 2, 3, 4, 5};
    int[] list2 = new int[3];
    System.arraycopy(list, 2, list2, 0, 3);
  3. clone 方法

    1
    2
    int[] list = {1, 2, 3, 4, 5};
    int[] list2 = list.clone();

可变长参数列表

... 来表示数组可变参数,参数数目不确定但类型确定的情况。

如果方法中有多个类型参数,可变长参数必须是最后一个参数

1
2
3
public static void wtf(int opt, int...arr){
System.out.println(arr.length);
}

Arrays 工具类

它包含了大量静态方法,常用如下:

1
2
3
int[] arr = new int[]{4, 1, 3, 2};
Arrays.sort(arr);
System.out.println(Arrays.toString(arr));

命令行参数

在命令行通过给 main 方法传递参数 String[] args ,其中参数均为字符串,但不一定需要用 "" 引起来

默认情况下(命令行中不传递参数),args 参数长度为 0 的空数组(并非 null

Chapter9. 对象和类

  • 对象通过引用变量访问,该变量包含对象的引用地址

    也就说,对象引用变量实际上只包含了对象的引用地址,并不是存放了一个对象

    System.arraycopy() 方法是 native 方法,性能优于普通方法

  • 只有当类中无明确声明构造方法时,才会自动生成默认无参构造方法。

    也就说,类若提供了有参构造方法,但没有提供无参构造方法时,调用有参的构造方法合法,但调用无参的构造方法,会编译错误!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class Test {
    int a;
    void Test(){
    System.out.println("233");
    }
    Test(int i){
    System.out.println(i);
    }
    public static void main(String[] args) {
    Test a = new Test(); //编译错误!因为无参构造方法没有自动生成
    }
    }
  • 一个类里可包含多个初始化块。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class Test {
    { //第二个执行(每创建一个对象,就)
    System.out.println("init-block");
    }
    static { //第一个执行(每个类只执行一次)
    System.out.println("static");
    }
    public Test() { //第三个执行
    System.out.println("WTFFFF");
    }
    public static void main(String[] args) {
    Test test = new Test();
    }
    }

  • 关于日期类 Calender

    1
    2
    3
    GregorianCalendar cal = new GregorianCalendar();
    System.out.println(new SimpleDateFormat("yyyy-MM-dd hh:mm:ss EEEE").format(cal.getTime()));
    //2021-06-06 10:17:45 星期日

    此处有个小坑点:

    1
    2
    Date d = new Date(12346788765); //编译错误,Integer number too large
    Date d = new
  • 关于 Random 类:

    java.utl 包含 DateRandom。而 java.lang 包含 SystemMath

    • 有参构造方法的参数类型是 long,故:

      1
      Random g = new Random(3.4); //编译错误
    • 返回的值为[0,x)[0, x)

    • 若两个Random 对象有相同的种子,那它们将产生相同的数列。

      因为其无参构造方法使用当前时间已经逝去的时间作为种子

  • Java 垃圾回收的工作原理:

    • 若一个对象被赋为空引用或没有使用,就成为 garbage 回收的候选者;如下:

      1
      2
      3
      4
      5
      String s1 = new String("hello");
      String s2 = new String("good");
      s1 = s2; //此时字符串对象"hello"符合垃圾收集条件
      String sb = new String("WTF");
      sb = null; //此时"WTF"对象会因符合垃圾收集条件而被销毁

      准确地来说,当没有任何活的线程能够访问一个对象时,该对象就符合垃圾收集条件。凭借此,程序员可以指定垃圾回收器回收对象。

    • 垃圾回收程序并不会马上回收该对象,而是在 JVM 探测出应用程序的内存不足的时候,或者是在 JVM 闲置时才进行回收;

      垃圾回收器不能保证有足够的的内存(无法保证 Java 程序从不产生内存溢出),它只能保证可用内存尽可能得到高效管理。

  • 你无法强迫垃圾回收器立即执行回收动作,但你可在 Java 程序请求 (只是建议它)JVM 运行垃圾收集器,如:

    1
    2
    Runtime rt = Runtime.getRuntime();
    rt.gc(); //请求垃圾收集

    另外,finalize() 方法(继承自 Object),能够在对象刚要被垃圾收集删除之前运行一些代码

    调用 finalize() 实际上能够导致对象免于被删除,比如说,在 finalize() 方法内可以编写代码把一个对象传递给另一个对象,
    能够有效地阻止该对象符合垃圾收集条件。

    但是,任何情况下都无法强迫垃圾回收器立即执行回收操作,同样地,任何指定对象的 finalize() 方法可能运行,但是不能指望它。

  • Java 虚拟机内存模型:

    方法区和 Java 堆 是所有线程共享的数据区,其他均为线程私有的。

  • Java 堆:几乎所有的对象和数组都是在堆中分配空间的。Java堆分为新生代和老年代连个部分,新生代用于存放刚刚产生的对象和年轻的对象,如果对象一直都没回收,生存得足够长,老年对象就会被移入到老年代。

    image-20210409001841386
  • 静态方法不能够访问非静态数据域,非静态方法能够访问静态数据域

  • 当程序执行下面第三行代码时,由于第一次使用 Student 类,则系统会在第一次使用该类时加载这个类,并初始化这个类,完成初始化后,系统将在堆内存Student 类分配了一块内存区(其中也包含了类属性 totalStudents,并设置初值) 。接着创建一个 Student 对象,并将其赋给了 s 变量。

    1
    2
    3
    4
    5
    public class Test {
    public static void main(String[] args) {
    Student s = new Student("JoyDee", 20);
    }
    }
  • public 修饰符能够使被修饰的成员或顶级类,能够被所有类访问,不论是否处于同一包中。

    default 通常被称为 包权限,即位于同一包中的类之间可互相访问其 default 成员和方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Fa {
    int a;
    }

    class Son extends Fa {
    public void test(){
    System.out.println(a);
    }
    }

    class WTF{
    public void test(){
    Son s = new Son();
    System.out.println(s.a);
    //同一包下,WTF类能够访问Son从Fa继承下来的a变量
    }
    }
  • package 语句作为 Java 源文件的第一条语句,指明该文件中定义的类所在的包。如下:

    1
    2
    3
    4
    5
    package JoyDee.weekFourTeen;
    //该文件中所在的类位于 ./JoyDee/weekFourTeen 目录下
    public class Test {
    }

    一个 Java 源文件只能包含一个 package 语句,但可包含多个 import 语句

  • 数据封装的好处——保持类的不变式,即构造方法和修改器都要求保证每个对象初始时一定要满足某个合法条件,而且是整个生命周期中都满足这个条件。如下对比:

    1
    2
    3
    4
    5
    6
    7
    8
    public void setSides(double s1, double s2, double s3){
    if(s1 < 0 || s2 < 0 || s3 < 0) throw new IllegalArgumentException();
    if((s1 + s2 > s3) && (s1 + s3 > s2) && (s2 + s3 > s1)){
    this.side1 = s1;
    this.side2 = s2;
    this.side3 = s3;
    }
    }

    除此之外,其他好处见PPT(ch9 P141)

  • 关于单例类,其特点在于:

    暂时咕咕咕(P152)

Chapter 10. 面向对象的思考

类之间的关系

  1. 依赖:A访问B的属性或方法,或者类A负责实例化类B

  2. 关联:A能够对B做什么

    牢记下面这张图:

    关联关系和聚集关系统称为组合。

    • 聚集(has-a):关联的特殊形式,一个对象可被多个其他聚集对象所拥有

    • 组成(Composition):聚合的加强形式,一个对象只归属于一个聚集对象。整体与部分的关系,部分不能离开整体而单独存在

  3. 泛化:一种继承关系。

  4. 实现:一种类与接口的关系

包装类

  • char 的包装类为 Characterint 的包装类为 Integer

  • 包装类没有无参数构造方法,所有的包装类的实例都是不可变的。

    注意,包装类一定有 String 的有参构造方法,另一有参构造方法的参数是它对应的基本数据类型,如下:

    1
    2
    Byte b = new Byte((byte) 123);
    Byte c = new Byte(123); //错误!!Byte没有Byte(int)的有参构造方法
    1
    2
    3
    4
    5
    6
    7
    8
    Integer x = new Integer(23); //Integer(int value)
    Integer y = new Integer("23")//Integer(String s)
    Integer z = Integer.valueOf(233); //static方法:valueOf(int x)
    Integer q = Integer.valueOf("233"); //static方法,传入String参数

    int w = y.intValue(); //Integer转换为int基本类型
    double v = y.doubleValue(); //Integer转换为double基本类型
    int bit16 = Integer.parseInt("-FF", 16); //第二个参数是进制,returns -255

    坑点:

    1
    2
    3
    4
    Integer i = Integer.parseInt("23", 8); //正确,自动装箱
    Double d = new Double(); //错误,没有无参构造方法
    double j = (Double.valueOf("23.4")).intValue(); //正确,强制类型转换
    double f = (Double.valueOf("23.4")).toString(); //错误
  • 装箱一般发生在赋值和方法调用中

    编译器在生成类的字节码时,插入必要的方法调用,虚拟机只是执行这些字节码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Integer x = 3 + new Integer(5); //正确
    Integer y = 3.0 //错误,double不可能自动装箱为Integer,需要强制转换

    double z = 3.4;
    System.out.println(z.intValue()); //错误,基本类型不可能调用包装类的方法

    Integer x = new Integer(2);
    Integer y = new Integer(3);
    System.out.println(x * y); //6
    System.out.println(x.floatValue()); //2.0
  • 包装类型的比较一定要使用 equals ,使用 == 比较两个包装对象,取决于是否值在 -128 到 127

    由源码的说明,如果 int 型参数 iIntegerCache.lowIntegerCache.high 范围内,则直接由 IntegerCache 返回(即直接从缓存中取出);否则 new 一个新的对象返回。

    1
    2
    3
    4
    Integer a = 9999;
    Integer b = 9999;
    System.out.println(a.equals(b)); //true
    System.out.println(a == 9999); //true

String

  • 由于 String 是引用对象类型,故其对象存储的并非是字符串的值,而是它的对象引用

  • 注意

    1
    2
    3
    4
    5
    String x = "Java";
    x.concat("2333");//ignored
    x = x.concat("Rules!");
    x.toLowerCase(); //ignored
    System.out.println(x); //JavaRules!
  • 当编译器遇到 String 常量时,会先检查该池内是否已存在相同的,若有就将新的引用指向现有String ,它们会在内存中共享同一块地址。

  • 注意常量池坑点:

    1
    2
    3
    4
    5
    String s = "Java";
    String s1 = "Java";
    String s2 = "Ja" + "va";
    System.out.println(s == s1); //true
    System.out.println(s == s2); //true

    "Ja""va" 都是字符串常量,当一个字符串由多个字符串常量连接而成时,它本身也是字符串常量

  • 检查一个字符串既不是 null ,也不是空串。

    1
    if(str.length() != 0 && str != null)
  • 字符串转换为数组:

    1
    char[] chars = "Java".toCharArray();

    数组转换为字符串:

    1
    2
    String str1 = new String(new char[]{'J', 'a', 'v', 'a'});
    String str2 = String.valueOf(new char[]{'J', 'a', 'v', 'a'});
  • String 类提供几个 valueOf 静态方法,可将 charchar[]doublelongintfloat 等数值转换为字符串

  • 注意 equals

    1
    2
    3
    String s1 = "Hello";
    String s2 = new String("Hello");
    boolean ans = s1.equals(s2); //true
  • StringBuilderStringBuffer 都能存储指定容量的字符串(若超出,则自动扩大)

    单任务访问:StringBuilder,多任务并发访问:StringBuffer

    1
    2
    3
    4
    5
    StringBuilder s0 = "0123"; //错误!!!!
    System.out.println(s1.substring(0, 2)); //区间为[0,2)
    StringBuilder s1 = new StringBuilder("0123456789");
    s1.insert(3, "zzzz"); //第一个参数为偏移长度,故012zzzz3456789
    s1.delete(0, 2); //区间为[0,2)

    trimTosize() 能够将 StringBuilder 对象的容量降到实际大小

  • 关于subString()

  • 关于 compareTo()

    1
    2
    3
    String s1 = "Wec";
    String s2 = "Pro";
    System.out.println(s1.compareTo(s2)); //7

Chapter 11. 继承和多态

OOP语言的三个典型特征:封装、继承、多态

super 调用父类的构造方法

  • 注意:父类的构造方法不能够被子类继承,若要调用父类构造方法,必须使用关键字 super ,且必须放在构造方法的第一行

  • 若子类没有显式地使用 super ,则父类无参构造方法将被自动地执行。

    Java程序在创建某个类的对象时,会隐式地创建该类的父类对象,只要有一个子类对象存在,则一定存在一个与之对应的父类对象。

构造方法链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Test {
public static void main(String[] args) {
Son s = new Son();
}
}

class Pa {
public Pa() {
System.out.println("I am a grandpa"); //先执行
}
}

class Fa extends Pa{
public Fa() {
System.out.println("I am a father"); //再执行
}
}

class Son extends Fa {
public Son() {
System.out.println("I am a son"); //最后执行
}
}

坑点:

1
2
3
4
5
6
7
8
9
10
11
class Fa {
public Fa(int a) {
System.out.println("I am a father");
}
}

class Son extends Fa {
public Son() {
System.out.println("I am a son");
} //此时编译错误,由于子类构造方法隐式调用了父类的无参构造方法,而Fa不存在无参构造方法(声明了有参构造方法就不会自动生成了),
}

若一个类要拓展,应提供一个无参构造方法以避免编程错误。

重写父类的方法

注意,重写都是针对实例方法,不能是类方法。因此若父类和子类都定义相同的静态方法,父类方法将会被隐藏

“两同两小一大”规则:

  • 两同:方法名相同,形参列表相同
  • 两小——返回值更加地具体:子类方法返回值类型\subseteq 父类;子类方法声明抛出的异常类型\subseteq 父类
  • 一大——访问权限:子类方法的访问权限\supseteq 父类

注意要点:

  • 子类对象无法访问父类被重写的方法,但可在子类方法体中调用
  • 当父类方法具有 private 访问权限,则该方法对其子类是隐藏的,既不能访问,更不能重写
  • 父类的构造方法不允许调用可被子类覆盖的方法,否则有可能抛出空指针异常

多态及动态绑定

多态:若编译时类型和运行时类型不一致,则出现所谓的多态。

多态是指当系统A访问B的服务时,系统B可通过多种实现方式来提供服务,而这一切对系统A是透明的。

其存在的条件如下:

  • 存在于继承中

  • 必须有方法的覆盖

    对象的属性不具有多态性,因此访问数据域或静态方法时,引用变量所声明的类型在编译时就已经决定了使用哪个数据域或静态方法了。

动态绑定的例子:若 GraduateStudent 中没有定义 toString 方法,JVM会向上搜索,找到父类 Student,结果发现了 toString 方法,就会调用该方法。

牢记下面的程序运行结果!

引用变量在编译阶段只能调用其编译时类型所具有的方法,但运行时则执行它所运行时的类型所具有的方法

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
public class Test {
public static void main(String[] args) {
Son son = new Son();
System.out.println("年龄为" + son.age); //20
son.baseMethod(); //执行从父类继承的run方法
son.run(); //执行当前类的run方法
son.sonMethod(); //执行当前类的sonMethod方法

Fa tmp = new Son(); //发生多态,编译类型为Son,运行类型为Son
System.out.println("年龄为" + tmp.age); //50,小心坑点!!因为多态只发生在方法上!
tmp.baseMethod(); //父类的普通方法
tmp.run(); //注意!!!子类将父类的run方法覆盖了
//tmp.sonMethod(); 无法调用,必须通过强制类型转换成运行时类型,如下所示
((Son) tmp).sonMethod(); //向下转型
}
}

class Fa {
public int age = 50;
public void baseMethod(){
System.out.println("父类的普通方法");
}
public void run(){
System.out.println("父类的run方法");
}
}
class Son extends Fa {
public int age = 20;
@Override
public void run(){
System.out.println("子类将父类的run方法覆盖了");
}
public void sonMethod(){
System.out.println("子类才拥有的方法!");
}
}

引用类型的强制转换

  • 父类->子类:引用类型间的转换中,父类 -> 子类,且需要强制类型转换。其前提是,该引用类型实际必须是子类的实例才可以(也就说,编译类型为父类,运行类型为子类),否则会发生ClassCastException,如下:

    1
    2
    3
    Fa peo = new Nephew();
    //Son son = (Son)peo; 抛出异常
    Nephew nephew = (Nephew)peo;
  • 子类->父类:当将一个子类对象赋给父类引用变量时,被称为向上转型,自动地完成

注意以下坑点:

1
((Fa)new Son()).run(); //子类将父类的run方法覆盖了,执行运行类型的方法

instanceof 运算符

  • 格式:引用类型的变量 instanceof 类名

  • 在进行强制类型转换之前,可通过 instanceof 运算符判断源对象是否为目标的实例,从而避免出现上述异常

  • 它用于判断该对象实际运行的类型(父类或祖先类,也满足条件),而不是看编译时的类型

    1
    2
    3
    4
    Animal animal = new Animal();
    Cat c = new Cat();
    System.out.println(animal instanceof Animal); //true
    System.out.println(animal instanceof Dog); //false

final修饰符

(一)修饰两类变量:两类被 final 修饰的变量,必须被初始化,遵循着不可多次赋值或修改的原则

注意,静态的 finalstatic final,(因为 final 必须要有初始化),要么在定义时初始化,要么就在静态初始化块赋值(因为 static),此时就不能够在构造方法中赋值了!

1
2
3
4
5
6
7
8
9
10
class Ben {
private final int x; //基本类型常量,除非是通过构造方法(且只能赋值一次),其他方法不能修改
public Ben() {
this(0); //合法
x = 1; //编译出错!final修饰的常量只能被赋值一次!
}
public Ben(int x) {
this.x = x;
}
}
1
2
3
4
5
6
7
8
public class Test {
private int idx = 233;
public static void main(String[] args) {
final Test cur = new Test(); //引用类型的常量
cur.idx = 666; //合法,允许修改引用变量所引用的Test对象的idx属性
cur = new Test(); //编译出错,不能改变引用变量cur引用的对象
}
}

(二)修饰方法:对父类的某个方法声明为 final 类型时,子类不能够覆盖该方法,如下:

1
2
3
4
public class Object{
public final Class getClass(){...}
//....
}

(三)修饰类:用final 修饰的类,不能够有子类,即不能被继承。

1
public final class Math{...}

ArrayList

若将子类放入到定义为父类泛型的 ArrayList 中,将会丢失子类本身的数据类型。若要调用子类的方法,需要进行强制类型转换。

数组与 ArrayList 的异同:

Chapter 12. 异常

异常分类图

  • Error 类以及子类表示程序本身无法修复,编译器不会检查他们,其错误一般由Java虚拟机抛出,此时程序就会被终止。

    一般不会通过继承 Error 类来创建用户自定义的错误类

  • RuntimeException 运行时异常(免检异常):当出现这类异常,即便没有捕获或者抛出,也会编译通过。一旦出错,则建议终止程序,如:

    通常是执行了错误操作,需要改进程序设计

    1
    2
    3
    int[] arr = {1, 2, 3};
    System.out.println(arr[4]);
    //Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException ...
  • 其他 Exception 类以及子类均属于编译时异常(受检异常),即Java编译器会检查它,当程序中可能出现这类异常时,要么用 try-catch 语句捕获,要么用 throws 子句声明抛出,否则编译不会通过

    通常是能够被修复的,不会导致整个程序的终止

异常跟踪栈

异常跟踪栈与“方法调用栈”方向相反,异常先从发生异常的方法逐渐向外传播,首先传给该方法的调用者,然后再向上传…直至最后传给 main 方法。

main 依然没有处理异常,则JVM会中止该程序,并打印异常的跟踪栈。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class SelfException extends Exception{
public SelfException() {
}
public SelfException(String message) {
super(message);
}
}
public class Test {
public static void main(String[] args) throws SelfException{
firstMethod();
}
public static void firstMethod() throws SelfException{
secondMethod();
}
public static void secondMethod() throws SelfException{
thirdMethod();
}
public static void thirdMethod() throws SelfException{
throw new SelfException("我是异常,来自thirdMethod");
}
}

结果显示如下:

异常处理方式

①声明一个异常;②抛出一个异常;③捕获一个异常

  • throws 声明可能会出现的异常,若 main 方法不知道如何处理,则可以使用 throws 声明抛出给 JVM 进行处理——打印异常跟踪栈,终止程序运行。

  • try 块被执行一次,则 try 块后只有一个 catch 块会被执行(除非使用循环)

  • 若存在 try 块时,总会执行 finally

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class Test {
    public static void main(String[] args) {
    try{
    throw new IOException();
    } catch (IOException e){
    System.out.println("1111");
    } finally {
    System.out.println("2222");
    }
    System.out.println("3333");
    }
    }
    //输出:
    //1111
    //2222
    //3333
  • 注意finally 语句不被执行的唯一情况是先执行了用于终止程序(终止当前Java虚拟机进程)的 System.exit() 方法。

    即便在 trycatch 代码块中的 return 语句时,只要有 finally 代码块,都会执行。

    再注意,finally 代码块不能通过重新给变量赋值的方式来改变 return 语句的返回值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class Test {
    private static int x = 2;
    public static void main(String[] args) throws Exception {
    System.out.println(woc()); //i = 1
    System.out.println(x); //x = 3
    }
    public static int woc(){
    int i = 1;
    try{
    throw new IOException();
    }catch (IOException e){
    return i;
    }finally {
    ++i;
    x++;
    System.out.println("In the final block: " + i); //i = 2
    }
    }
    }

Chapter 13. 抽象类和接口

抽象类定义

有抽象方法的类只能定义为抽象类,抽象类可以没有抽象方法。

1
2
3
4
abstract class 类名 {
public abstract 返回类型 方法名 (参数列表);//抽象方法不能有方法体
//...
}

抽象类可以包含属性、方法(普通方法和抽象方法均可)、构造方法(只能是普通方法,不能被 abstract 修饰)。

抽象类的构造方法不能用于创建实例,主要用于被子类调用

以下三种情况,只能定义为抽象类:

  • 类中定义了一个抽象方法
  • 继承了抽象父类,但没实现父类包含的所有抽象方法
  • 实现了接口,但没实现接口包含的所有抽象方法

abstract关键字

abstract 不能用于修饰构造方法。

此外:

  • 对于类或者方法的修饰, finalabstract 不能够同时使用
  • 对于方法, staticabstract 不能够同时使用
  • 对于方法,privateabstract 不能够同时使用

接口定义

相比于抽象类,接口更加特殊,不能够包含普通方法,其所有方法都是抽象方法。

1
2
3
4
5
6
[修饰符] interface 接口名 {
0到n个常量定义...
0到多个抽象方法定义...
0到多个内部类、接口、枚举定义...
0到多个私有方法、默认方法(实例方法)或类方法定义....(JDK 1.8 之后)
}

接口的声明的属性默认(不论是否显式声明)为 public static final ,同时,接口中定义的方法默认 public abstract (即抽象方法),举例如下:

接口中无构造方法和初始化,故接口里定义的属性只能在定义时指定默认值

1
2
3
4
5
6
public interface People {
int id = 1; //相当于 public static final int id = 1
void start(); //相当于 public abstract void start()
void run(); //相当于 public abstract void run()
void stop(); //相当于 public abstract void stop()
}

这里有小坑点:

1
2
3
4
5
6
7
8
interface A {
void m();
}
class B implements A {
void m(){ //根据”两小一大“,方法m的访问权限(大)必须是public
System.out.println("2333");
}
}

另外,一个类不能继承多个类,但一个接口可能继承多个接口。

Comparable接口

1
2
3
public interface Comparable<T>{
public int compareTo(T o);
}

实现 Comparable 接口:

1
2
3
4
5
public class 类名 implement Comparable<类名>{
public int CompareTo(T other){
//...
}
}

Cloneable 接口

Cloneable 接口是空的,称为标记接口。

1
2
public interface Cloneable {
}

类只有当实现了 Cloneable 接口(即标记为可复制的),其对象内部才能够使用 Object中定义的 clone 方法实现克隆功能。

若没有实现该接口,就调用 Objectclone() 会抛出异常

该方法如下:

1
protected native Object clone() throws CloneNotSupportedException;

由于是 protected ,只能在实现该接口的对象的方法内部调用,并且需要对该方法进行覆盖。如下调用。

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
public class Test {
private int idx = 233;
public static void main(String[] args) {
Fa c = new Fa();
System.out.println(c.hashCode()); //989110044
System.out.println(c.date.hashCode()); //-194351978
Fa a = (Fa)c.clone();
System.out.println(a.hashCode()); //424058530
System.out.println(a.date.hashCode()); //-194351978
}
}

class Fa implements Cloneable{
public int age = 50;
public Date date = new Date();
@Override
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
}

注意这个过程中的浅拷贝与深拷贝

接口与抽象类的相同点

接口与抽象类的不同点

接口抽象类
成员数据必须常量既可常量,也可变量
成员方法不能有方法体的普通方法完全可以包含普通方法
构造方法不能够包含可以包含,但不能被 abstract 修饰,且不能用来创建实例
共同的根Object
初始化块不能可以
实现与继承一个类可直接实现多个接口,一个接口可继承多个接口一个类最多只有一个直接父类

李氏替换原则

主要用于判断两个类之间是否符合继承关系。父类出现的地方,子类可以将其替换,且程序的行为不会发生改变。

可以理解为,若一个类继承了另一个类,两者的行为应该是一致的。子类不能添加任何父类没有的附加约束(比如正方形在设置长度或宽度时要受到长宽相等的约束)。

若违背该原则,应该将两个类共有属性及方法提炼出来作为抽象父类,各自继承父类并重写属于自己的方法