Java核心技术阅读笔记(第五章)
Chapter 5 继承
作者:Denis
版本:1.0
编写时间:2022/10/16
编写地点:中国山西省
5.1 类、超类和子类
如果一个类继承自另一个类,那么这个类被称为子类,被继承的类被称为超类。
继承的关系实际上是特殊与一般的关系。例如,老师和学生都属于人,但他们的具体角色是不同的。
在Java中,使用extends关键字表示继承关系,所有的继承都是公共继承。
具体来说,使用继承可以使子类获得比父类更强的功能,同时也具有父类的某些行为。
子类可以增加方法、字段或者覆盖超类的方法来达到增强超类的功能。
子类覆盖超类的方法
子类中的某些方法与超类的方法签名相同,就称为方法覆盖。需要注意,子类方法的返回类型需要与超类方法返回类型一致或者兼容
例如超类方法返回类型double,子类方法返回类型为double或者int都行。
同时,子类方法不能低于父类方法的可见性,例如超类方法访问修饰符protected,子类方法访问修饰符为public或者protected都行。
关键字super
super的功能与this非常类似,super可以调用父类的构造器,也可以调用父类暴露出的方法与字段。
区别是,super不是一个对象引用,可以将this赋值给一个对象变量,但这对super来说是错误的操作。
子类构造器
子类构造器的代码必须显著地调用超类构造器,语句是super(parameter...)
。倘若存在超类的无参构造器,才可以不声明,但实际上还是调用了(可以通过javap命令查看)。总之,调用子类构造器必定会调用一次超类构造器。而且,这条语句必须位于子类构造器的第一句。
多态
超类的对象变量既能够接收超类对象,也可以接收子类对象。反之,子类对象变量无法接收超类对象。当子类存在对超类方法覆盖时,超类对象变量会自动地执行对应类的方法。
多态的原理是可以判断对象运行时所属类,并生成每个类的可使用的方法表(可用的方法既有自己定义的,也有继承超类的方法)。如果使用超类的对象变量接收子类对象,例如Person p = new Student(...)
,这个对象编译类型是Person,而运行时类型为Student。在调用p的方法时,会查看Student类的方法表,如果这个方法恰好子类重写了,那么执行其子类的该方法。而对于调用p的字段,则调用其超类字段,而非子类字段。
下面时是对多态行为的演示。
import java.time.*;
public class ManagerTest
{
public static void main(String[] args)
{
// construct a Manager object
var boss = new Manager("Carl Cracker", 80000, 1987, 12, 15);
boss.setBonus(5000);
var staff = new Employee[3];
// fill the staff array with Manager and Employee objects
staff[0] = boss;
staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
staff[2] = new Employee("Tommy Tester", 40000, 1990, 3, 15);
// print out information about all Employee objects
for (Employee e : staff)
System.out.println("name=" + e.getName() + ",salary=" + e.getSalary());
}
}
class Manager extends Employee
{
private double bonus;
/**
* @param name the employee's name
* @param salary the salary
* @param year the hire year
* @param month the hire month
* @param day the hire day
*/
public Manager(String name, double salary, int year, int month, int day)
{
super(name, salary, year, month, day);
bonus = 0;
}
public double getSalary()
{
double baseSalary = super.getSalary();
return baseSalary + bonus;
}
public void setBonus(double b)
{
bonus = b;
}
}
class Employee
{
private String name;
private double salary;
private LocalDate hireDay;
public Employee(String name, double salary, int year, int month, int day)
{
this.name = name;
this.salary = salary;
hireDay = LocalDate.of(year, month, day);
}
public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public LocalDate getHireDay()
{
return hireDay;
}
public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
}
}
阻止继承:final类和方法
如果给类、方法定义添加final关键字,则代表该类无法被继承、重载
将一个类添加final关键字后,所有的方法都自动隐式添加final,但字段不会被隐式添加final
字段添加final意味着创建对象后该字段的值(对于基本类型)无法修改,字段指向的对象(对于引用)无法重新指向新的对象。
向上转型和向下转型
向上转型,例如Person p = new Student()
向下转型,例如Student s = (Student)p;
向下转型需要确保该对象的实际类型确实是接收变量的所属类型,否则会报类型转换错误。
为此,在进行向下转型时,需要判断该对象的实际类型,使用instanceof关键字可以进行判断
注意:null不是任何类的对象,使用 null instanceof Object
永远返回false
5.2 抽象类
在类的声明中添加abstract修饰符,即可让该类成为抽象类,抽象类不能产生任何抽象类实例对象。
抽象类对象变量可以接收一个子类的对象
在方法的声明中添加abstract修饰符,可以只给出方法签名,具体的实现依靠子类
包含一个或多个抽象方法的类必须被声明为abstract类型的,如果没有抽象方法,也可以被声明为抽象类
抽象类可以包含字段和具体的方法以及构造器,下面是一个抽象类的代码
public abstract class Person
{
public abstract String getDescription();
private String name;
public Person(String name)
{
this.name = name;
}
public String getName()
{
return name;
}
}
5.3 Object类
Object类是所有类的超类,有必要研究Object类的方法
equals方法
用于判断当前对象和接收的参数是否为同一个对象。但对于基于状态比较两个对象的相等性,需要在类中编写自己的equals方法来覆盖Object类的equals方法。下面给出一个equals方法的具体实现。
public boolean equals(Object otherObject)
{
// a quick test to see if the objects are identical
if (this == otherObject) return true;
// must return false if the explicit parameter is null
if (otherObject == null) return false;
// if the classes don't match, they can't be equal
if (getClass() != otherObject.getClass()) return false;
// now we know otherObject is a non-null Employee
var other = (Employee) otherObject;
// test whether the fields have identical values
return Objects.equals(name, other.name)
&& salary == other.salary && Objects.equals(hireDay, other.hireDay);
}
在这个例子中,最核心的比较语句是最后一句,为了防止空指针异常(对象中的某些值可能为null),使用了更安全的Objects.equals(a, b)
方法。这个方法如果两个参数都为null返回true,只有一个参数为null返回false,否则就调用a.equals(b)
注意,覆盖Object类的equals方法最好添加注解@Override,注意形参为Object类型的参数
如果超类有编写好的equals方法,子类无需从头编写,可以先调用超类的equals方法,然后再比较其独有的字段即可。
Java语言对equals方法要求如下特性:
- 对称性,
a.equals(b) == b.equals(a)
- 自反性,
a.equals(a) == true
- 传递性,
if(a.equals(b) && b.equals(c)) a.equals(c)
a.equals(null) == false
对于数组的比较,可以使用Arrays.equals
方法
hashCode方法
hashCode是由对象导出的一个整型值(可以为负数),如果是两个不同对象,那么它们的散列码一般不相同,反之,如果两个对象通过equals测试,那么它们地hashCode应该相同。
通过该方法产生的散列码值一般用于插入到散列表中。
Object类的hashCode方法由对象的存储地址导出
如果重新定义equals方法,那么就需要重新定义hashCode方法。应该合理组合散列码,以便产生地更加均匀。
String类的hashCode方法设计如下。
int hash = 0;
for(int i = 0; i < length(); i++)
hash = 31 * hash + charAt(i);
自己编写的类的hashCode可以像这样
public int hashCode(){
return 7 * name.hashCode() + 11 * Double.hashCode(salary) + 13 * hireDay.hashCode();
}
最好使用null安全的方法(因为对象中某些字段的值为null,对此调用hashCode()会产生空指针异常),Objects类提供了这个方法。
Objects.hashCode
会判断参数是否为null,如果为null,就返回0,否则对参数调用其类的hashCode方法
需要组合多个散列值时,可以用Objects.hash(...)
。它会对每个参数调用Objects.hashCode
方法。
对于数组类型的字段,可以使用Arrays.hashCode
,这个方法得到的散列值由数组元素的散列码组成
5.4 ArrayList类
java中,数组可以再运行时确定数组大小,但是确定大小后就不能修改
ArrayList类可以在空间满的时候进行扩容
ArrayList是一个有类型参数的泛型类,类型参数放在一对尖括号中,如ArrayList<Person>
下面的程序演示了ArrayList类的使用
import java.util.*;
public class ArrayListTest
{
public static void main(String[] args)
{
// fill the staff array list with three Employee objects
var staff = new ArrayList<Employee>();
staff.add(new Employee("Carl Cracker", 75000, 1987, 12, 15));
staff.add(new Employee("Harry Hacker", 50000, 1989, 10, 1));
staff.add(new Employee("Tony Tester", 40000, 1990, 3, 15));
// raise everyone's salary by 5%
for (Employee e : staff)
e.raiseSalary(5);
// print out information about all Employee objects
for (Employee e : staff)
System.out.println("name=" + e.getName() + ",salary=" + e.getSalary() + ",hireDay="
+ e.getHireDay());
}
}
ArrayList类的用法
- 添加元素,使用
add
方法 - 移除元素,使用
remove
方法 - 如果事先能够估计出存储的元素数量,在填充数组前使用
ensureCapacity
方法,或者让构造器接收一个正数参数。 - 返回实际元素的个数,使用
size
方法 - 如果确定当前ArrayList空间将不会发生变化,可以使用
trimToSize
方法,调用后会把多余的未利用空间回收。 - 改变元素,使用
set
方法,并提供索引值与新值【set不能代替add来添加元素】 - 获得元素,使用
get
方法,并提供索引值 - 将ArrayList对象元素拷贝到普通数组中,使用
ToArray
方法
ArrayList类在添加和移除元素后,这个元素后面的元素都需要往前或者往后移动,如果需要经常在中间添加、删除元素,而且ArrayList中元素很多的情况下,使用ArrayList效率并不高,这时应该使用链表。
5.5 自动装箱与自动拆箱
基本类型都有其对应的包装器类型,所有的包装器都是final,包装器可以接收基本类型的赋值。
Integer i = 277;
实际上,这条语句编译后被自动替换为
Integer i = Integer.valueOf(277);
这被称为自动装箱
相反,如果把一个Integer值赋值给基本类型int,称为自动拆箱
Integer i = 277;
int age = i;
//实际上编译后被自动替换为 int age = i.intValue();
如果一个条件表达式混用了Integer和Double,那么Integer会拆箱为int,转为double,再装箱为Double
包装器类型的比较
两个值相同的包装器对象,如果使用==进行判断,结果可能为true,也可能为false
自动装箱规范要求介于-128和127之间的short和int装在固定的对象中,在这个范围内的Integer对象进行==比较一定相等
最好使用equals方法来消除这些不确定的因素。
将字符串转为整型
使用Integer.parseInt(String s)
即可,对于其他包装器也是这个办法。但前提是String的含义真的是一个数字。
Integer类的其他方法
toString(int i, int radix)
,返回一个指定进制的i值的字符串parseInt(String s, int radix)
,返回一个指定进制的i值valueOf(String s, int radix)
,返回一个指定进制的i值的Integer对象