## 5.1.4 继承层次
java不支持多重继承,但提供了一些类似多重继承的功能
## 5.1.5 多态
有一个简单规则来判断是否应该将数据设计为继承关系 这就是is-a规则,它指出子类的每个对象也是超类的对象,例如每个经理也是员工\
程序中出现超类对象的任何地方都可以使用子类对象替换
```
Employee e;
e = new Employee(...);
e = new Manager(...);
```
java中,对象变量是多态的
```
Manager boss=new Manager(...);
Employee[] staff=new Employee[3];
staff[0]=boss;
```
在这个例子中,变量staff[0]和boss引用同一个对象,但是编译器只将staff[0]看成是一个Employee对象,这意味着
```
boss.setBonus(5000);//Ok
staff[0].setBonus(5000);//Error
```
因为setBonus不是Employee类的方法\
另外不可以将超类的引用赋给子类变量
## 5.1.6 理解方法调用
假设要调用x.f(args),隐式参数x声明为类c的一个对象,下面为调用详细过程:\
1. 编译器查看对象的声明类型和方法名,编译器会列举类c中所有名为f的方法和其超类中名为f并且可访问的方法(超类私有方法不可访问)
2. 编译器确定方法调用中提供的参数类型并挑选完全匹配的,这个过程称为重载解析\
备注:返回类型不是签名的一部分,不过在覆盖一个方法时,需要保证返回类型的兼容性,允许子类将覆盖方法的返回类型改为原返回类型的子类型,这称为可协变的返回类型
```
public Employee getBuddy(){...}
public Manager getBuddy(){...}
```
3. 如果是private、static、final或构造器方法,那么编译器可以准确知道调用哪个方法。这称为静态绑定
4. 虚拟机预先为每个类计算了一个方法表,搜索表即可
代码中关于调用e.getSalary()的解析过程:
1. 虚拟机获取e的实际类型方法表
2. 查找定义了getSalary()签名的类
3. 调用方法
tip:覆盖一个方法时,子类方法不能低于超类方法的可见性
## 5.1.7 组织继承:final类和方法
不允许扩展的类称为final类
类中的某个方法可以声明为final,这样子类就不能覆盖这个方法
final类中的所有方法自动成为final方法(不包括字段)
## 5.1.8 强制类型转换
```
double x=3.1415;
int x2=(int) x;
```
如上所示,将浮点类型强制转换为整数,舍弃小数部分
同样可以将某个类的对象引用转换为另一个类的对象引用
```
Manager boss=(Manager) staff[0];
```
在ManagerTest类中,由于某些元素是普通员工,所以staff数组必须是Employee对象数组。需要将数组中引用经理的元素复原成Manager对象,才能访问子类新特性。
若将子类引用赋给超类变量,编译器是允许的。若将一个超类引用赋给一个子类变量,则必须进行强制类型转换。
另外,在进行强制类型转换前,首先用instanceof来检查是否可行
```
if(staff[1] instanceof Manager)
{
boss=(Manager)staff[1];
...
}
```
综上所述,首先只能在继承层次内进行强制类型转换,其次转换前用instanceof操作符来检查是否可行。
## 5.1.9 抽象类
学生类和雇员类都是人,可以抽象出人类
```
public abstract class Person
{
private String name;
public Person(String name)
{
this.name=name;
}
public abstract String getDescription();
public String getName()
{
return name;
}
}
```
抽象方法充当占位方法的角色,他们在子类中具体实现,扩展抽象类有两种选择。一种在子类中保留抽象类中的部分或所有抽象方法仍未定义,这样就需要将子类也标记为抽象类。另一种则定义全部方法,子类非抽象。
抽象类不能实例化,可以定义一个抽象类的对象变量,但是这样的变量只能引用非抽象子类的对象。这里的p是一个抽象类型Person的变量,它引用了一个非抽象子类Student的实例。
```
Person p=new Student("Vince Vu","Economics");
```
下面定义一个扩展抽象类Person的具体子类Student
```
public class Student extends Person
{
private String major;
public Student(String name,String major)
{
super(name);
this.major=major;
}
public String getDescription()
{
return "a student major in"+major;
}
}
```
### PersonTest.java
```
package javaSX.abstractClasses;
public class PersonTest
{
public static void main(String[] args)
{
var people=new Person[2];
people[0]=new Employee("Harry",50000,1989,10,1);
people[1]=new Student("Maria","computer science");
for(Person p:people)
{
System.out.println(p.getName()+","+p.getDescription());
}
}
}
```
### Person.java
```
package javaSX.abstractClasses;
public abstract class Person
{
public abstract String getDescription();
private String name;
public Person(String name){
this.name=name;
}
public String getName()
{
return name;
}
}
```
### Employee.java
```
package javaSX.abstractClasses;
import java.time.*;
public class Employee extends Person
{
private double salary;
private LocalDate hireDay;
public Employee(String name,double salary,int year,int month,int day)
{
super(name);
this.salary=salary;
this.hireDay=LocalDate.of(year,month,day);
}
public double getSalary()
{
return this.salary;
}
public LocalDate getHireDay()
{
return this.hireDay;
}
public void raiseSalary(double percent)
{
double raise=this.salary*percent/100;
this.salary+=raise;
}
public String getDescription()
{
return String.format("an employee with a salary of $%.2f",salary);
}
}
```
### Student.java
```
package javaSX.abstractClasses;
public class Student extends Person
{
private String major;
public Student(String name,String major)
{
super(name);
this.major=major;
}
public String getDescription()
{
return "a student majoring in"+major;
}
}
```
## 5.1.10 受保护方法
1. 仅对本类可见——private
2. 对外部完全可见——public
3. 对本包可见——protected
4. 对本包可见——默认,无需修饰符
# 5.2 Object:所有类的超类
## 5.2.1 Object类型变量
可以使用Object类型的变量引用任何类型的对象
```
Object obj=new Employee("harry",35000);
```
object类型变量只能作为一个泛型容器,要想执行具体操作,需要知晓对象原始类型,并进行强制类型转换
```
Employee e=(Employee) obj;
```
## 5.2.2 equals方法
Object中实现的equals方法将确定两个对象引用是否相等。
```
public class Employee
{
...
public boolean equals(Object otherObject)
{
if(this==otherObject) return true;
if(otherObjecct==null) return false;
if(getClass()!=otherObject.getClass()) return false;
Employee e=(Employee) otherObject;
return name.equals(e.name)&&salary==e.salary&&hireDay.equals(e.hireDay);
}
...
}
```
getClass()会返回对象所属的类
为了防止name和hireDay为null的情况,需要使用Object.equals方法。\
如果两个参数都为null,Object.equals将返回true。\
如果其中一个参数为null,则返回false.\
若两个参数都不为null,则可以调用a.equals(b)
```
return Object.equals(name,e.name)&&salary==e.salary&&Object.equals(hireDay,e.hireDay);
```
在子类中定义equals方法时,首先调用超类equals方法,再比较子类独有字段
```
public class Manager extends Employee
{
...
public boolean equals(Object otherObject)
{
if(!super.equals(otherObject)) return false;
Manager m=(Manager) otherObject;
return bonus==m.bonus;
}
...
}
```
## 5.2.3 相等测试与继承
若子类可以有自己的相等性概念,则对称性需求将强制使用getClass检测\
如果由超类决定相等性概念,那么就可以使用instanceof检测,这样可以在不同子类的对象之间进行相等性比较。
在员工与经理的例子中,如果需要比较子类经理的奖金,我们就要使用getClass来检测。如果使用员工ID来比较,就可以使用instanceof,并且应该将Employee.equals生命为final
下面列出书写equals方法的步骤建议
1. 显式参数命名为otherObject,稍后需要进行强制转换
2. 检测this与otherObject是否相等
```
if(this==otherObject) return true;
```
3. 检测otherObject是否为null,如果为null,返回false。
```
if(otherObject==null) return false;
```
4. 比较this与otherObject的类,如果equals的语义可以在子类中改变,则使用getClass()检测
```
if(getClass()!=otherObject.getClass()) return false;
```
如果所有子类都有相同的equals语义,可以使用instanceof检测
```
if(!(otherObject instanceof ClassName)) return fasle;
```
5. 将otherObject强制转换为相应类型变量
```
ClassName other=(ClassName) otherObject;
```
6. 最后比较字段,使用==比较基本类型,使用Object.equals比较对象类型
```
return Object.equals(name,other.name)&&salary==other.salary&&Object.equals(hireDay,other.hireDay);
```
如果在子类中重新定义equals,则要在其中使用super.equals(otherObject),再进行强制类型转换\
对于数组类型的字段,可以使用静态的Arrays.equals方法检测相应的数组元素
## 5.2.4 hashCode方法
散列码是由对象导出的整型值,可正可负。
```
var s="ok";
var sb=new StringBuilder(s);
System.out.println(s.hashCode()+" "+sb.hashCode());
var t=new String("ok");
var tb=new StringBuilder(t);
System.out.println(t.hashCode()+" "+tb.hashCode());
```
字符串s和t有相同的散列码,因为String的散列码由内容导出\
但sb与tb有着不同的散列码,因为StringBuilder没有定义hashCode,Object的hashCode默认从存储地址导出。
equals与hashCode必须要相容,如果x.equals(y)返回true,那么x.hashCode()就必须和y.hashCode()返回相同的值
## 5.2.5 toString方法
Employee中的toString
```
public String toString()
{
return getClass().getName()+"[name="+name+", salary="+salary+", hireDay="+hireDay+"]";
}
```
Manager中的toString
```
public class Manager extends Employee
{
...
public String toString()
{
return super.toString()+"[bonus="+bonus+"]";
}
...
}
```
随处可见toString()方法的原因在于,只要对象与一个字符串通过操作符“+”连接起来,Java编译器就会自动调用toString方法来获得这个对象的字符串描述,例如
```
var p=new Point(10,20);
String message="Current point is "+p;
```
+p 也就意味着调用p.toString(), 对于基本数据类型同样适用
打印数组可以使用Arrays.toString,多维数组可以使用Arrays.deepToString
### EqualsTest.java
```
package javaSX.equals;
public class EqualsTest {
public static void main(String args[])
{
var alice1=new Employee("Alice",75000,1987,12,15);
var alice2=alice1;
var alice3=new Employee("Alice",75000,1987,12,15);
var bob=new Employee("Bob",50000,1989,10,1);
System.out.println("alice==alice2:"+(alice1==alice2));
System.out.println("alice==alice3:"+(alice1==alice3));
System.out.println("alice.equals(alice3):"+(alice1.equals(alice3)));
System.out.println("alice.equals(bob):"+(alice1.equals(bob)));
System.out.println("bob.toString():"+bob);
var carl=new Manager("Carl",80000,1987,12,15);
var boss=new Manager("Carl",80000,1987,12,15);
boss.setBonus(5000);
System.out.println("boss.toString() "+boss);
System.out.println("carl.equals(boss) "+carl.equals(boss));
System.out.println("alice1.hashCode() "+alice1.hashCode());
System.out.println("alice3.hashCode() "+alice3.hashCode());
System.out.println("bob.hashCode() "+bob.hashCode());
System.out.println("carl.hashCode() "+carl.hashCode());
}
}
```
### Employee.java
```
package javaSX.equals;
import java.time.*;
import java.util.Objects;
public 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;
this.hireDay=LocalDate.of(year,month,day);
}
public String getName()
{
return this.name;
}
public double getSalary()
{
return this.salary;
}
public LocalDate getHireDay()
{
return this.hireDay;
}
public void raiseSalary(double percent)
{
double raise=this.salary*percent/100;
this.salary+=raise;
}
public String getDescription()
{
return String.format("an employee with a salary of $%.2f",salary);
}
public int hashCode()
{
return Objects.hash(name,salary,hireDay);
}
public String toString()
{
return getClass().getName()+"[name="+name+",salary="+salary+",hireday="+hireDay+"]";
}
public boolean equals(Object otherObject)
{
if(this==otherObject) return true;
if(otherObject==null) return false;
if(getClass()!=otherObject.getClass()) return false;
var other=(Employee) otherObject;
return Objects.equals(name,other.name)&&salary==other.salary&&Objects.equals(hireDay,other.hireDay);
}
}
```
### Manager.java
```
package javaSX.equals;
public class Manager extends Employee{
private double bonus;
public Manager(String name,double salary,int year,int month,int day)
{
super(name,salary,year,month,day);
this.bonus=0;
}
public void setBonus(double bonus)
{
this.bonus=bonus;
}
public int hashCode()
{
return java.util.Objects.hash(super.hashCode(),bonus);
}
public boolean equals(Object otherObject)
{
if(!super.equals(otherObject)) return false;
var other=(Manager) otherObject;
return other.bonus==this.bonus;
}
public String toString()
{
return super.toString()+" bonus:"+bonus;
}
public double getSalary()
{
double baseSalary=super.getSalary();
return baseSalary+bonus;
}
}
```
# 5.3 泛型数组列表
运用ArrayList来解决动态数组问题,ArrayList是一个有类型参数的泛型类
## 5.3.1 声明数组列表
声明并构造一个保存Employee对象的数组列表
```
ArrayList<Employee> staff=new ArrayList<>();
```
使用add来添加元素,使用staff.remove(i)来删除元素
```
staff.add(new Employee("Harry",...));
staff.add(new Employee("Tony",...));
```
数组列表管理一个内部的对象引用数组,数组空间可能用尽,再次使用add时,会自动创建一个更大的数组列表,并拷贝现有元素。
```
staff.ensureCapacity(100);
ArrayList<Employee> staff=new ArrayList<>(100);
```
前者在声明构建暂时确定数组列表空间,后者在声明构建的时候暂时确定\
staff.size()返回数组列表当前元素个数。
## 5.3.2 访问数组列表元素
不能使用[]访问或改变数组列表的元素,要使用get和set方法。
set方法是替换元素,首先需要要通过add来添加元素。
Employee e=staff.get(i);可以获得数组列表第i个元素
使用for each循环遍历数组列表
```
for(Employee e:staff)
{
...//do something with e
}
//上下两种循环一致
for(int i=0;i<staff.size();i++)
{
Employee e=satff.get(i);
...//do something with e
}
```
# 5.4 对象包装器与自动装箱
所有基本类型都有一个与之对应的类,称之为包装器,如下所示\
Integer,Long,Float,Double,Short,Byte,Character和Boolean\
包装器类是不可改变的,即不能改变其中的值\
包装器类还是final 即不能派生他们的子类
```
var list=new ArrayList<Integer>
list.add(3);
int n=list.get(i);
```
自动装箱:list.add(3)会自动变换成list.add(Integer.valueOf(3));\
自动拆箱:list.get(i)会自动变换成list.get(i).inValue();
==运算符可以应用于包装器类对象,不过检测的是对象是否有相同的内存地址,因此下面的比较通常会失败
```
Integer a = 1000;
Integer b = 1000;
if(a==b) ...
```
自动装箱规范要求boolean,byte,char<=127,介于-128和127之间的short和int被包装到固定的对象中。若在前面的例子中赋值为100,那么比较结果一定成功。
如果在一个表达式中混用Integer和Double,Integer值就会拆箱,提升为double,再装箱为Double\
要想将字符串转换成整形,可以使用下面的语句\
这里与Integer对象没有关系,parseInt是一个静态方法
```
int x=Integer.parseInt(s);
```
# 5.5 参数数量可变的方法
printf方法是这样定义的
```
public class PrintStream
{
public PrintStream printf(String fmt,Object... args)
{
return format(fmt,args);
}
}
```
省略号是代码的一部分,printf接受两个参数,一个是格式字符串,另一个是Object[] 数组。
自定义一个函数计算若干数值中的最大值
```
public static double max(double... values)
{
double largest=Double.NEGATIVE_INFINITY;
for(double v:values)
{
if(v>largest)
{
largest=v;
}
}
return largest;
}
double m=max(3.1,40.05,-8.6);//调用方式
```
# 5.6 枚举类
一个类的对象是有限且固定的,这种情况下我们使用枚举类
```
package javaSX.enums;
import java.util.*;
public class EnumTest {
public static void main(String[] args)
{
var in=new Scanner(System.in);
System.out.print("Enter a size:(SMALL,MEDIUM,LARGE,EXTRA_LARGE) ");
String input=in.next().toUpperCase();
Size size=Enum.valueOf(Size.class, input);
System.out.println("size="+size);
System.out.println("abbreviation="+size.getAbbreviation());
if(size==Size.EXTRA_LARGE)
{
System.out.println("Good Job");
}
in.close();
}
}
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 this.abbreviation;
}
}
```
1. enum和class、interface的地位一样
2. 使用enum定义的枚举类默认继承了java.lang.Enum,而不是继承Object类。枚举类可以实现一个或多个接口。
3. 枚举类的所有实例都必须放在第一行展示,不需使用new 关键字,不需显式调用构造器。自动添加public static final修饰。
4. 使用enum定义、非抽象的枚举类默认使用final修饰,不可以被继承。
5. 枚举类的构造器只能是私有的。
# 5.8 继承的设计技巧
1. 将公共操作和字段放在超类中
2. 不要使用受保护字段
3. 使用继承实现“is-a”关系