
「JavaSE学习笔记10」Java 易错归纳
¶Chapter 1 概述
¶Java跨平台
Java 既具有解释型语言的特征,也具有编译型语言的特征。
Java既不是编译型语言也不是解释型语言,还是静态语言。
Java语言经过编译器编译后生成与平台无关的字节码文件(*.class,需要由Java解释器来解释执行)
不同操作系统有不同的虚拟机,但提供一个相同的面向编译器的接口。
只要为不同平台实现了相应的虚拟机,编译之后的Java字节码就可以在该平台上运行

¶编译与运行Java源文件

举例如下:

java命令运行文件,紧跟于java文件中的类名,如java HelloWorld(注意,java命令针对于 class 文件,但是在输入命令时不加.class后缀)
¶main方法
签名:public static void main(String[] arg)
其中,public 与 static 前后顺序可互换,但 public 可以省略不写,static 必须要使用
另外,返回值只能是 void ,否则该方法不是程序的入口方法
若写成
Main方法,Java解释器无法找到main方法
¶Java源文件
一个Java文件只能有一个
public类,且文件名必须与public类名一致注意,每个源文件最多只有一个包声明,必须位于 Java 源文件的第一行
Java程序中所有的关键字都是小写,无须任何大写字母
¶编程错误
- 语法错误
- 运行错误
- 逻辑错误
¶Chapter 2
¶Java标识符

所有关键字都是由小写英文字母组成的
¶基本数据类型
true和false只能小写⭐表达式类型的自动提升:一个算术表达式包含多个基本类型的值时,整个算术表达式的数据类型都将发生自动提升,规则如下:
- 所有
byte、short、char类型自动提升到int - 自动提升到表达式中最高等级操作同样的类型
1
2
3
4
5
6
7
8
9byte 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 | |
¶字符编码
四种等价赋值方式:
1 | |
¶字符型与数值型数据的转换
当一个整数转换成 char 型数据时,只使用整数的低十六位,其他部分忽略,如下:
1 | |
若操作数是一个数字或字符,char 自动转换为数字,如下:
1 | |
对于字符串:
对于
+运算符,若操作数之一不是字符串,则非字符串自动转换为字符串,注意下方坑点:1
2
3
4
5System.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类型兼容:byte、short、char、int,又或者是字符串,又或者是枚举类型注意,
long类型以及浮点类型,均不能够自动地转换为int类型对于
case valueN子句中,valueN必须是常量
¶random 方法
故:
- 返回:
a + Math.random() * b - 返回:
(int)(10 + Math.random()*10) - 随机返回 0 或 1:
(int)(Math.random()*10)%2
¶Chapter 6. 方法
¶方法签名
方法名+参数列表
¶方法的重载
方法的重载与方法返回值类型、修饰符等没有任何关系。
因此,被重载的方法必须具有不同的参数列表,不能基于不同的修饰符或返回值类型(否则编译错误)
关于方法的匹配,你
¶static修饰符
静态方法不能够直接调用非静态成员变量和非静态方法,经典错误如下:

¶方法体
对于有返回值的方法,
return语句必需的,尽管代码逻辑正确,但编译器并不认为是正确的(编译错误)
对变量初始化时,注意下面的坑点:

¶方法调用
一个方法结束运行会返回调用者,同时其活动记录中所有变量会从栈中移出(释放所有变量),定义在方法中的变量(包括引用类型)也结束其声明周期
¶方法中参数传递
- 一个方法不能修改一个基本数据类型的参数
- 一个方法可改变一个对象参数的状态
- 一个方法不能够让对象参数引用一个新的对象
¶Chapter 7. 一维数组
¶数组的初始化
实际数组元素被存储在堆heap内存中,数组引用变量是引用类型变量,被存储在栈内存中
静态初始化:开发人员显式地指定每个数组元素的初始值,系统决定长度
1
2
3int[] arr = new int[]{1, 2, 3, 4, 5}; //注意不要漏了中括号
int
float[] f = {1.2f, 3, 4f, 5}; //注意坑点动态初始化:开发人员只指定数组长度,系统为数组分配初始值
1
2int[] mylist; //声明整形的数组
mylist = new int[5]; //创建数组并将数组的引用赋值给mylist注意:不能同时使用静态初始化和动态初始化
¶数组的长度
注意普通数组的长度为:arr.length;而 String 的长度为:arr.length(),此外:"Hello".length()
¶数组的复制
循环语句遍历赋值
使用
System类中的静态方法arraycopy():内容相同,引用不同1
2
3int[] list = {1, 2, 3, 4, 5};
int[] list2 = new int[3];
System.arraycopy(list, 2, list2, 0, 3);clone方法1
2int[] list = {1, 2, 3, 4, 5};
int[] list2 = list.clone();
¶可变长参数列表
... 来表示数组可变参数,参数数目不确定但类型确定的情况。
如果方法中有多个类型参数,可变长参数必须是最后一个参数
1 | |
¶Arrays 工具类
它包含了大量静态方法,常用如下:
1 | |
¶命令行参数
在命令行通过给 main 方法传递参数 String[] args ,其中参数均为字符串,但不一定需要用 "" 引起来

默认情况下(命令行中不传递参数),args 参数长度为 0 的空数组(并非 null)
¶Chapter9. 对象和类
对象通过引用变量访问,该变量包含对象的引用地址
也就说,对象引用变量实际上只包含了对象的引用地址,并不是存放了一个对象

System.arraycopy()方法是native方法,性能优于普通方法只有当类中无明确声明构造方法时,才会自动生成默认无参构造方法。
也就说,类若提供了有参构造方法,但没有提供无参构造方法时,调用有参的构造方法合法,但调用无参的构造方法,会编译错误!
1
2
3
4
5
6
7
8
9
10
11
12public 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
15public 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
3GregorianCalendar 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
2Date d = new Date(12346788765); //编译错误,Integer number too large
Date d = new关于
Random类:java.utl 包含
Date、Random。而 java.lang 包含System、Math有参构造方法的参数类型是
long,故:1
Random g = new Random(3.4); //编译错误返回的值为
若两个
Random对象有相同的种子,那它们将产生相同的数列。因为其无参构造方法使用当前时间已经逝去的时间作为种子
Java 垃圾回收的工作原理:
若一个对象被赋为空引用或没有使用,就成为
garbage回收的候选者;如下:1
2
3
4
5String 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
2Runtime rt = Runtime.getRuntime();
rt.gc(); //请求垃圾收集另外,
finalize()方法(继承自Object),能够在对象刚要被垃圾收集删除之前运行一些代码调用
finalize()实际上能够导致对象免于被删除,比如说,在finalize()方法内可以编写代码把一个对象传递给另一个对象,
能够有效地阻止该对象符合垃圾收集条件。但是,任何情况下都无法强迫垃圾回收器立即执行回收操作,同样地,任何指定对象的
finalize()方法可能运行,但是不能指望它。Java 虚拟机内存模型:

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

静态方法不能够访问非静态数据域,非静态方法能够访问静态数据域

当程序执行下面第三行代码时,由于第一次使用
Student类,则系统会在第一次使用该类时加载这个类,并初始化这个类,完成初始化后,系统将在堆内存为Student类分配了一块内存区(其中也包含了类属性totalStudents,并设置初值) 。接着创建一个Student对象,并将其赋给了s变量。1
2
3
4
5public 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
17class 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
5package JoyDee.weekFourTeen;
//该文件中所在的类位于 ./JoyDee/weekFourTeen 目录下
public class Test {
}一个 Java 源文件只能包含一个
package语句,但可包含多个import语句数据封装的好处——保持类的不变式,即构造方法和修改器都要求保证每个对象初始时一定要满足某个合法条件,而且是整个生命周期中都满足这个条件。如下对比:
1
2
3
4
5
6
7
8public 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. 面向对象的思考
¶类之间的关系
依赖:A访问B的属性或方法,或者类A负责实例化类B

关联:A能够对B做什么

牢记下面这张图:

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

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

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

泛化:一种继承关系。

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

¶包装类
char的包装类为Character;int的包装类为Integer包装类没有无参数构造方法,所有的包装类的实例都是不可变的。
注意,包装类一定有
String的有参构造方法,另一有参构造方法的参数是它对应的基本数据类型,如下:1
2Byte b = new Byte((byte) 123);
Byte c = new Byte(123); //错误!!Byte没有Byte(int)的有参构造方法1
2
3
4
5
6
7
8Integer 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
4Integer 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
10Integer 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型参数i在IntegerCache.low和IntegerCache.high范围内,则直接由IntegerCache返回(即直接从缓存中取出);否则new一个新的对象返回。1
2
3
4Integer a = 9999;
Integer b = 9999;
System.out.println(a.equals(b)); //true
System.out.println(a == 9999); //true
¶String
由于
String是引用对象类型,故其对象存储的并非是字符串的值,而是它的对象引用注意
1
2
3
4
5String x = "Java";
x.concat("2333");//ignored
x = x.concat("Rules!");
x.toLowerCase(); //ignored
System.out.println(x); //JavaRules!当编译器遇到
String常量时,会先检查该池内是否已存在相同的,若有就将新的引用指向现有的String,它们会在内存中共享同一块地址。
注意常量池坑点:
1
2
3
4
5String 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
2String str1 = new String(new char[]{'J', 'a', 'v', 'a'});
String str2 = String.valueOf(new char[]{'J', 'a', 'v', 'a'});String类提供几个valueOf静态方法,可将char、char[]、double、long、int和float等数值转换为字符串注意
equals1
2
3String s1 = "Hello";
String s2 = new String("Hello");
boolean ans = s1.equals(s2); //trueStringBuilder或StringBuffer都能存储指定容量的字符串(若超出,则自动扩大)单任务访问:
StringBuilder,多任务并发访问:StringBuffer1
2
3
4
5StringBuilder 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
3String s1 = "Wec";
String s2 = "Pro";
System.out.println(s1.compareTo(s2)); //7
¶Chapter 11. 继承和多态
OOP语言的三个典型特征:封装、继承、多态
¶super 调用父类的构造方法
注意:父类的构造方法不能够被子类继承,若要调用父类构造方法,必须使用关键字
super,且必须放在构造方法的第一行。若子类没有显式地使用
super,则父类无参构造方法将被自动地执行。Java程序在创建某个类的对象时,会隐式地创建该类的父类对象,只要有一个子类对象存在,则一定存在一个与之对应的父类对象。

¶构造方法链
1 | |
坑点:
1 | |
若一个类要拓展,应提供一个无参构造方法以避免编程错误。
¶重写父类的方法
注意,重写都是针对实例方法,不能是类方法。因此若父类和子类都定义相同的静态方法,父类方法将会被隐藏
“两同两小一大”规则:
- 两同:方法名相同,形参列表相同
- 两小——返回值更加地具体:子类方法返回值类型 父类;子类方法声明抛出的异常类型 父类
- 一大——访问权限:子类方法的访问权限 父类
注意要点:
- 子类对象无法访问父类被重写的方法,但可在子类方法体中调用
- 当父类方法具有
private访问权限,则该方法对其子类是隐藏的,既不能访问,更不能重写 - 父类的构造方法不允许调用可被子类覆盖的方法,否则有可能抛出空指针异常
¶多态及动态绑定
多态:若编译时类型和运行时类型不一致,则出现所谓的多态。
多态是指当系统A访问B的服务时,系统B可通过多种实现方式来提供服务,而这一切对系统A是透明的。
其存在的条件如下:
存在于继承中
必须有方法的覆盖
对象的属性不具有多态性,因此访问数据域或静态方法时,引用变量所声明的类型在编译时就已经决定了使用哪个数据域或静态方法了。
动态绑定的例子:若 GraduateStudent 中没有定义 toString 方法,JVM会向上搜索,找到父类 Student,结果发现了 toString 方法,就会调用该方法。
牢记下面的程序运行结果!
引用变量在编译阶段只能调用其编译时类型所具有的方法,但运行时则执行它所运行时的类型所具有的方法
1 | |
¶引用类型的强制转换
父类->子类:引用类型间的转换中,父类 -> 子类,且需要强制类型转换。其前提是,该引用类型实际必须是子类的实例才可以(也就说,编译类型为父类,运行类型为子类),否则会发生
ClassCastException,如下:1
2
3Fa peo = new Nephew();
//Son son = (Son)peo; 抛出异常
Nephew nephew = (Nephew)peo;子类->父类:当将一个子类对象赋给父类引用变量时,被称为向上转型,自动地完成
注意以下坑点:
1 | |
¶instanceof 运算符
格式:
引用类型的变量 instanceof 类名在进行强制类型转换之前,可通过
instanceof运算符判断源对象是否为目标的实例,从而避免出现上述异常它用于判断该对象实际运行的类型(父类或祖先类,也满足条件),而不是看编译时的类型
1
2
3
4Animal animal = new Animal();
Cat c = new Cat();
System.out.println(animal instanceof Animal); //true
System.out.println(animal instanceof Dog); //false
¶final修饰符
(一)修饰两类变量:两类被 final 修饰的变量,必须被初始化,遵循着不可多次赋值或修改的原则
注意,静态的
final即static final,(因为final必须要有初始化),要么在定义时初始化,要么就在静态初始化块赋值(因为static),此时就不能够在构造方法中赋值了!
1 | |
1 | |
(二)修饰方法:对父类的某个方法声明为 final 类型时,子类不能够覆盖该方法,如下:
1 | |
(三)修饰类:用final 修饰的类,不能够有子类,即不能被继承。
1 | |
¶ArrayList
若将子类放入到定义为父类泛型的 ArrayList 中,将会丢失子类本身的数据类型。若要调用子类的方法,需要进行强制类型转换。
数组与 ArrayList 的异同:

¶Chapter 12. 异常
¶异常分类图

Error类以及子类表示程序本身无法修复,编译器不会检查他们,其错误一般由Java虚拟机抛出,此时程序就会被终止。一般不会通过继承
Error类来创建用户自定义的错误类RuntimeException运行时异常(免检异常):当出现这类异常,即便没有捕获或者抛出,也会编译通过。一旦出错,则建议终止程序,如:通常是执行了错误操作,需要改进程序设计
1
2
3int[] 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 | |
结果显示如下:

¶异常处理方式
①声明一个异常;②抛出一个异常;③捕获一个异常

throws声明可能会出现的异常,若main方法不知道如何处理,则可以使用throws声明抛出给 JVM 进行处理——打印异常跟踪栈,终止程序运行。若
try块被执行一次,则try块后只有一个catch块会被执行(除非使用循环)若存在
try块时,总会执行finally块1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public 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()方法。即便在
try或catch代码块中的return语句时,只要有finally代码块,都会执行。
再注意,
finally代码块不能通过重新给变量赋值的方式来改变return语句的返回值1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public 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 | |
抽象类可以包含属性、方法(普通方法和抽象方法均可)、构造方法(只能是普通方法,不能被 abstract 修饰)。
抽象类的构造方法不能用于创建实例,主要用于被子类调用
以下三种情况,只能定义为抽象类:
- 类中定义了一个抽象方法
- 继承了抽象父类,但没实现父类包含的所有抽象方法
- 实现了接口,但没实现接口包含的所有抽象方法
¶abstract关键字
abstract 不能用于修饰构造方法。
此外:
- 对于类或者方法的修饰,
final与abstract不能够同时使用 - 对于方法,
static与abstract不能够同时使用 - 对于方法,
private与abstract不能够同时使用
¶接口定义
相比于抽象类,接口更加特殊,不能够包含普通方法,其所有方法都是抽象方法。
1 | |
接口的声明的属性默认(不论是否显式声明)为 public static final ,同时,接口中定义的方法默认 public abstract (即抽象方法),举例如下:
接口中无构造方法和初始化,故接口里定义的属性只能在定义时指定默认值
1 | |
这里有小坑点:
1 | |
另外,一个类不能继承多个类,但一个接口可能继承多个接口。

¶Comparable接口
1 | |
实现 Comparable 接口:
1 | |
¶Cloneable 接口
Cloneable 接口是空的,称为标记接口。
1 | |
类只有当实现了 Cloneable 接口(即标记为可复制的),其对象内部才能够使用 Object 类中定义的 clone 方法实现克隆功能。
若没有实现该接口,就调用
Object的clone()会抛出异常
该方法如下:
1 | |
由于是
protected,只能在实现该接口的对象的方法内部调用,并且需要对该方法进行覆盖。如下调用。
1 | |
注意这个过程中的浅拷贝与深拷贝

¶接口与抽象类的相同点

¶接口与抽象类的不同点
| 接口 | 抽象类 | |
|---|---|---|
| 成员数据 | 必须常量 | 既可常量,也可变量 |
| 成员方法 | 不能有方法体的普通方法 | 完全可以包含普通方法 |
| 构造方法 | 不能够包含 | 可以包含,但不能被 abstract 修饰,且不能用来创建实例 |
| 共同的根 | 无 | Object |
| 初始化块 | 不能 | 可以 |
| 实现与继承 | 一个类可直接实现多个接口,一个接口可继承多个接口 | 一个类最多只有一个直接父类 |
¶李氏替换原则
主要用于判断两个类之间是否符合继承关系。父类出现的地方,子类可以将其替换,且程序的行为不会发生改变。
可以理解为,若一个类继承了另一个类,两者的行为应该是一致的。子类不能添加任何父类没有的附加约束(比如正方形在设置长度或宽度时要受到长宽相等的约束)。
若违背该原则,应该将两个类共有属性及方法提炼出来作为抽象父类,各自继承父类并重写属于自己的方法