0%

Java - 继承+

学习Java中有关继承以及与继承特性相关的其他内容的概念。

参考资料

《Java核心技术 卷1(第十一版)》

《Java基础核心总结》

菜鸟教程


类、子类和超类

Java中使用关键字 extends 表示继承。在Java中,所有继承都是公共继承。Java不支持多继承。

👉 方法的覆盖(重写)

👉 super关键字

超类对象变量可以引用子类对象。一个对象变量可以指示多种实际类型的现象称为多态。在运行时能够自动选择适当的方法,即动态绑定。在Java中,动态绑定是默认的行为(作为对比,C++通过为成员函数添加 virtual 修饰符实现动态绑定),如果不希望某个方法是“virtual”的,可以为其标上修饰符final

final类和final方法

👉 final关键字

不允许定义子类扩展的类称为final类。要定义final类,在定义类时添加修饰符final即可,final方法同理。当某个类定义为final类时,类中所有的方法便也成为final方法,final方法不能被子类重写。

字段也可以单独声明为final,final类的字段并不会自动成为final字段。

继承关系中的强制类型转换

父类对象变量可以直接引用子类对象。假如类B是类A的子类,那么:

1
2
3
4
var obj = new A[2];
B b = new B();
obj[0] = b; //OK
obj[1] = new A();

子类对象变量不能引用父类对象,在将父类对象变量的值赋给子类对象变量时,要使用强制类型转换。

1
2
b = (B)obj[0];	//OK
b = (B)obj[1]; //ERROR

并且在强制类型转换之前,应该先检查能否能进行类型转换。

1
2
3
4
5
if(obj[0] instanceof B)
{
b = (B)obj[0];
...
}

该强制类型转换语法类似C,但处理过程更像C++中的dynamic_cast操作。不同的是转换失败时,Java会抛出一个ClassCastException异常,而C++是返回一个null对象。

抽象类

通过在类定义时添加修饰符abstract来定义抽象类,抽象方法同理。

包含一个及以上抽象方法的类必须被声明为抽象的。但是即使没有抽象方法,类也可以声明为抽象类。

抽象类可以包含字段成员和具体方法。可以尽量将通用的字段和方法(无论抽象与否)放在父类(无论抽象与否)中。

当抽象类的某个子类将所有抽象方法都具体定义,就不再是抽象的。否则子类仍为抽象类。

抽象类不能实例化,但是抽象类对象变量可以引用非抽象的子类对象。


Object类

Object类是Java中所有类的超类,在Java中每个类都扩展了Object,所以Object类型对象变量可以引用任何类型的对象。

先学习两个Object中常用的方法。

equals方法

Object类中的equals方法用于检测一个对象是否等于另外一个对象。

Object类中实现的equals方法检查两个对象引用是否相等,对于很多类来说这样已足够。可以通过重写equals方法添加自己需要的相等判定条件,例如两个对象的状态相等。如果在子类中重新定义equals,需要在方法中添加一句super.equals(...)调用。

可能用到的方法

  • Object.equals(a, b):当ab都为 null 时,返回 true;当ab其中一个为 null 时,返回 false;ab均不为 null 时,将调用 a.equals(b);
  • a.getClass():返回对象a所属的类;
  • a instanceof A:当a引用对象为类A或者A的子类的实例时返回true。

toString方法

toString方法会返回表示对象值的一个字符串。定义子类的时候可以重写toString方法来更好的描述子类对象的内容。

可能用到的方法

  • String getName():返回这个类的名字;
  • Class getSuperclass():以Class对象的形式返回当前类的超类。

只要对象与一个字符串通过操作符“+”连接,Java编译器就会自动调用toString方法来获得这个对象的字符串描述。

1
System.out.println("Current position = " + position);

泛型数组列表

Java的数组大小可以在运行时才确定,但在确定后也不容易更改。泛型类 ArrayList 类似于数组,但是能在添加或删除元素时自动地调整数组的容量,使用时在ArrayList后面的尖括号<>内追加所需的数组元素类型。

ArrayList 只能存储引用型数据类型,所以不能直接存储Java的基本数据类型,而是存储其包装类。

声明数组列表

1
2
3
4
5
6
7
8
9
//数组列表对象的声明方法

ArrayList<Integer> arr = new ArrayList<Integer>(); //一般声明构造方法

var arr = new ArrayList<Integer>(); //在Java10及以后,可以使用var关键字

ArrayList<Integer> arr = new ArrayList<>(); //(菱形语法)也可以省略右边的类型参数

var arr = new ArrayList<>(); //在使用var时使用菱形语法会生成ArrayList<Object>而不是原来的ArrayList<Integer>

向数组列表中添加元素使用 add 方法。可以通过 ensureCapacity 方法给数组列表预设一个容量。size 方法返回数组列表实际元素个数。

1
2
3
arr.ensureCapacity(10);	//或者直接在声明时预设容量:ArrayList<Integer> arr = new ArrayList<>(10)
arr.add(3);
System.out.println(arr.size()); //arr.size() = 1

如果确定当前数组列表大小保持恒定,就可以调用trimToSize方法。该方法会将存储块的大小调整为保存当前元素数量所需要的存储空间,多余空间会被回收。

访问数组列表元素

访问和改变数组列表的元素,需要使用 getset 方法。注意,只有当数组列表的实际大小大于 i 时,才能调用 arr.set(i, ...)arr.get(i),即 set 方法只是用来修改数组列表的元素,添加元素得用 add 方法。

使用 toArray 方法可以将数组元素拷贝到一个数组中。

add(n, e) 方法将元素 e 插入原数组列表中,位置 n 及以后的数组元素都会向后移动一个位置;remove(n) 方法将位置 n 上的元素移除,数组长度-1。

for each 循环适用于数组列表。

类型化与原始数组列表的兼容性

可以将一个类型化的 ArrayList 对象赋给原始 ArrayList 变量(编译器可能会有警告),且不需要强制类型转换;反之编译器会报错,且报错不能通过强制类型转换解决。


对象包装器和自动装箱

上述的数组列表并不能直接存储Java的基本数据类型,即有时需要将基本数据类型转化为对象类型。

所有的基本数据类型都有与之对应的类,称为包装器。包装器类名字都对应其代表的基本数据类型:Integer、Long、Short、Float、Double、Byte、Boolean、Character,前六个派生于类Number。

包装器类是不可变的,一旦构造就不能修改其所包装的值,包装器类是final类,所以不能派生子类。

基本数据类型包装后可以使用 ArrayList ,但是效率不如直接构造数组。

1
2
var list = new ArrayList<Integer>();
list.add(3);

当向 list 中添加 int 类型的元素,例如 list.add(3) ,将自动变换成 list.add(Integer.valueOf(3)) ,这种变换称为自动装箱

相反地,将Integer对象赋给一个int值时,将会自动地拆箱,即把 int n = list.get(i) 转换成 int n = list.get(i).intValue()

自动装箱和拆箱也适用于算术表达式。

1
2
Integer n = 3;
n++;

如上,编译器将自动地插入一条对象拆箱的指令,然后进行自增计算,然后再将结果装箱。

Tips

  1. 注意,比较对象内存地址地运算符“==” 在比较两个包装器类的数值是否相等时,有时候是正确的。自动装箱规范要求boolean、byte、char ≤ 127,介于 -128 到 127 之间的 short 和 int 会被包装到固定的对象中,此时用 “==” 判断相等就一定能成功(能被引用的特定对象只一个)。
1
2
3
Integer a = 100;
Integer b = 100; // -127 < 100 < 127
if(a == b){...} //true
  1. 包装器类引用当然可以是null,装箱时可能会抛出 NullPointerException 异常;
  2. 如果在一个表达式中混合使用 Integer 和 Double 类型,Integer 会自动拆箱,提升为 double 之后再装箱为 Double。
1
2
3
Integer n = 2;
Double x = 2.0;
System.out.println(true ? n : X); // prints 1.0
  1. 包装器类包含了一些实用的静态方法,比如将字符串转换为整型 int x = Integer.parseInt(s);
  2. 包装器类对象都是不可变的,想要修改内容数值可以使用 org.omg.CORBA 包中定义的持有者类型,如 IntHolder、BooleanHolder 等。

可变参数方法

Java中同样可以提供参数可变的方法,例如 printf。

1
2
3
4
public class PrintStream
{
public PrintStream printf(String fmt, Object... args) {return format(fmt, args); }
}

省略号 表示这个方法可以接收任意数量的 Object 对象。

一个方法中只能指定一个可变参数,并且必须是方法的最后一个参数。

1
typename... parameterName	//可变参数的声明方法

允许将数组作为最后一个参数传递给可变参数的方法,并且,如果一个方法的最后一个参数是数组,可以把它重定义为包含可变参数的方法,这种行为不会破坏原有的代码。例如,将 main 方法写成 public static void main(String… args)


枚举类

定义枚举类型,实际上是定义了一个类。

1
2
3
4
5
6
7
8
9
public enum Size 
{
SMALL("S"), MEDIUM("M"), LARGE("L"), EXTRA_LARGE("XL");

private String abbreviation;

private Size(String abbreviation) {this.abbreviation = abbreviation; }
public String getAbbreviation() {return abbreviation; }
}

上述代码定义了一个 Size 类,这个类有4个实例,并且不能构造新的 Size 对象。因此,在比较两个 Size 类型对象变量是否相等时,可以直接使用 “==” 而不是 equals 方法。

如前例所示,可以给枚举类定义构造器、字段和方法。枚举类的构造器默认是 private 的,声明时可以省略修饰符(如果声明为 public 或者 protected 会报错)。

枚举类也可以声明在内部类中。所有的枚举值都是静态常量(public static final)。

枚举既可以包含具体方法,也可以包含抽象方法。 如果枚举类具有抽象方法,则枚举类的每个实例都必须实现它(在实例名后的{}内具体声明)。

所有的枚举类型都是 Enum 类的子类,Enum 类有一些实用的静态方法。

  1. toString:toString方法会返回当前的枚举常量名。

  2. valueOf:toString的逆方法,使用字符串赋值。如 Size s = Enum.valueOf(Size.class, "SMALL"); 将 s 设置成枚举常量 SMALL。

  3. values:返回一个包含该枚举类型的所有枚举值的数组。如 Size[] values = Size.values(); 返回数组 values。

  4. ordinal:返回某个枚举常量在枚举声明中的位置(从0开始)。如 Size.MEDIUM.ordinal() 返回1。

-------------本文结束感谢您的阅读-------------