2.1 类、超类、子类
如果一个类存在is-a的关系,例如上一节的Employee 和Manager类,每个经理都是员工,就存在这is-a的关系,“is-a”关系就是继承的一个明显特征。
2.1.1定义子类
用关键字extends表示继承
class Manager extends Employee
{
added methods and field
}
与c++不同的是,java中的继承都是公共继承,不存在c++中的保护继承和私有继承。
通过扩展超类定义子类的时候,只需指出子类与超类的不同之处,在设计类是,要将一般的方法放在超类中,将更特殊的方法放在子类中。
2.1.2覆盖方法
在超类中的某些方法在子类中并不适用,例如超类Employee中的getsalary方法,在Manager中并不适用,因为Manager中需要返回薪水和奖金的总和,此时,需要提供一个新的方法来覆盖超类中的这个方法
Employee:
public double getSalary() {
return salary;
}
但是Manager中的getSalary方法并不能只是简单的返回bonus和salary的和
public double getSalary() {
return bonus+salary;
}
这样写是不行的,因为salary是Employee的私有字段,Manager类并不能直接访问Employee中的私有字段,需要调用方法。
public double getSalary(){
return bonus+getSalary();
}
此方法仍然有问题,因为该方法中的getSalary()一直在调用自身,我们需要的是调用超类的此方法。
public double getSalary() {
double basesalary=super.getSalary();
return basesalary+bonus;
}
运用关键字super即可调用父类的getSalary()方法.
2.1.3子类构造器
最后为子类Manager提供一个子类构造器。
public Manager(String name,double salary,int year,int month,int day){
super(name,salary,year,month,day);
bonus=0;
}
此时super具有不同的含义。
super(name,salary,year,month,day);
指调用超类的具有name,salary,year,month,day参数的构造器(看参数类型)
如果子类中没有显示的调用父类的构造器,那么将会默认调用超类的无参构造。
关键字this:1、指示隐式参数的调用 2、调用该类的其他构造器
关键字super:1、调用超类的方法 2、调用超类的构造器
创建一个新经理
Manager boss=new Manager("李四",7000,2003,9,23)
boss.setBonus(5000)
下面定义一个包含三个员工的数组
Employee[] staff=new Employee[3]
在数组中混入经理和员工对象
staff[0]=new Employee("赵",2000,2001,9,29)
staff[1]=new Employee("钱",2000,2002,12,3)
staff[2]=boss
输出每个人的薪水
for(Employee employee:staff){
System.out.println(employee.getName()+" "+employee.getSalary())
}
该语句将会输出
赵 2000.0
钱 2000.0
李四 7000.0
尽管这里employee声明为Employee类型,但是实际上employee既可以引用Employee类型的对象也可以引用Manager类型的对象,虚拟机知道employee的实际引用的对象类型,因此可以正确的调用方法。
一个对象变量可以指示多种实际类型的现象称为多态,在运行是可以自动的选择适当的方法,称为动态绑定。
2.1.4继承层次
在java中只允许进行单继承,即一个子类只可以有一个超类,java不支持多重继承,但是提供了接口。
2.1.5 多态
判断数据是否应该设计为继承关系的一个简单规则就是is-a规则,他指出子类的对象都是超类的对象(每个经理都是员工)。is-a规则的另一种表述是替换原则,他指出程序中出现超类对象的任何地方都可以替换为子类对象。
例:可以使用子类对象给超类对象赋值
Employee e
e=new Employee("赵",2000,2001,9,29)
e=new Manager("李四",7000,2003,9,23)
在java中对象的变量是多态的任何一个Employee对象既可以引用一个Employee对象也可以引用一个Employee任何一个子类的对象。
在上述staff数组中,就利用了这个原则
Manager boss=new Manager("李四",7000,2003,9,23)
staff[2]=boss
这意味着可以这样调用 boss.setBonus(5000)
但是不可以这样调用 staff[2].setBonus(5000)
注意:不可以将超类的引用赋给子类变量
Manager m=staff[0];//ERROR
原因:并不是所有员工都是经理,若赋值成功,则m可能引用了一个不是经理的员工对象
2.1.6理解方法调用
假设要调用x.f(args)方法,隐式参数x为类C的一个对象,下面是调用过程
1、编译器查看对象的声明类型和方法名(f方法可能被重载),
至此编译器知道了所有可能被调用的候选方法
2、编译器确定方法调用中提供的参数类型。
如果在名为f的方法中存在一个与提供参数类型完全相符的函数,那么就选择这个方法,该过程称为重载解析
如果未找到参数相匹配的方法,或者发现有多个方法相符,就会报错。
这次,编译器已知道所有需要调用的方法的名字和参数类型
3、如果是private、static、final方法
编译器将会准确的知道调用哪个方法,此为静态绑定
如果调用的方法依赖于隐式参数的具体类型,会在运行时进行动态绑定
4、程序运行并且采用动态绑定时,虚拟机必须调用与x实际类型对应的方法,假设x的实际类型为D,D是C的子类
如果D中有f方法,调用,否则去超类C中去找,依次类推。
2.1.7阻止继承:final类和方法
有时一个类不允许被扩展,如果在定义类时使用了final修饰符就表明这个类是final类。
例:阻止Manager的子类Executive派生子类
public final class Executive extends Manager{}
当类中的方法被声明为final那么子类就不能覆盖此方法。
2.1.8 强制类型转换
将一个类型强制转换为另一个类型的过程称为强制类型转换。
例:
double x=3.14
int nx=(int)x
将表达式x的值强制转换为整数类型部分
使用强制类型转换的唯一原因是:要在暂时忽略对象的实际类型之后使用对象的全部功能,例如原来的staff数组是Employee对象的数组,我们需要将数组中引用经理的元素进行复原,以便于访问新增加的变量。
在将一个值存入一个变量时,编译器会检查是否承诺过多。如果一个子类的对象引用赋给了超类,编译器允许,但是将超类的引用赋给子类的时候就承诺过多了,必须进行强制类型转换才能通过运行时检查。
如果在继承链上进行向下(继承链上是超类)的强制类型转换,可能会出现错误。
例:
Manager boss=(Manager) staff[1]
此时就会触发ClassCastException异常,因此在进行强制类型转化时要进行类型检查。
if(staff[1] instanceof Manager){
boss=(Manager) staff[1]
}
综上:1、只能在继承层次内进行强制类型转换。
2、在将超类强制转换为子类之前,应该使用instanceof进行检查(如果一个类型和null进行比较会返回false,因为null未引用任何对象)
2.1.9 抽象类
在继承层次中,位于上层的类更加具有一般性,也更加抽象,从某种角度看,祖先类更具有一般性,人们一般只将它作为其他类的基类,而并不是去创造某种实例。
例如:Employee是一个类,Student也是一个类但是他们均属于Person类
也就可以提供一个更加高层次的类Person来作为他们的基类。
因为每个人都有姓名等属性,因此可以将getName等函数放入继承层次中更高的一层。
若再增加一个返回对一个人描述的函数getDescription(),那么在Employee和student中实现此方法很容易,但是在person中除了Name对这个人一无所知,此时可以用abstract关键字,那么在person中就不必实现此方法了。
为了提高程序的清晰度,包含一个或多个抽象方法的类本身必须被声明为抽象的。
public abstract class Person{
public abstract String getDescription();
}
抽象类中还可以包含字段以及具体方法
扩展抽象类有两种选择:
1、在子类中仍然保留部分或所有的抽象方法未定义,那么此时该子类也许定义为抽象类。
2、在子类中定义全部方法,此时,子类就不是抽象的了。
注:即使不含抽象类的方法也可以定义为抽象类
抽象类不可以创建具体的对象(即不可以new)
如:new Person("name");是错误的
但是可以声明一个抽象类的引用使其指向一个具体的子类
如:Person p=new Student();
此时p是一个Person类型的变量,其引用了一个非抽象类Student的实例
Person[] people=new Person[2];
people[0]=new Employee("张三",5000.123455,2003,12,4);
people[1]=new Student("李四","高中");
for(Person p:people){
System.out.println(p.getDescription());
}
该代码中,p.getDescription(),调用的不是抽象类Person的对象,而是像Employee和student具体子类的对象,这些对象中都实现了getDescription()方法,但是不可以省略超类Person中的抽象方法,那样就不可以通过变量p进行调用。
2.1.10 受保护访问
在一个类中,最好将字段标记为private,而方法标记为public,但是有时会希望超类中的某个方法只允许子类访问,后者更少见的,只希望子类中的某些方法访问超类的某个字段,为此需要将这些类方法或字段声明为受保护(protected)。
在java中保护字段只能由同一个包中的类访问。
Java中的4种访问控制修饰符小结:
1、仅对本类可见---private
2、对外部完全可见---public
3、对本包和所有子类可见---protected
4、对本包可见---默认,不需要修饰符
2.2 Object:所有类的超类
Object类是Java中所有类的始祖,在Java中每个类都扩展了Object类,但并不需要明确的指出继承关系。
即不需要: public class Employee extends Object,如果没有明确的指出超类,Object就被认为是这个类的超类
2.2.1 Object类型的变量
可以使用Object类型的变量引用任何类型的对象
Object obj=new Employee("Harry",15000)
Object类型的变量值能用于作为各种值的一个泛型容器。要想对其中的内容进行操作,必须进行强制类型转换
Employee e=(Employee) obj
在Java中只有基本数据类型不是对象,例:数值、字符和布尔类型的值都不是对象
所有的数组类型,不管是对象数组还是基本类型的数组都扩展了Object类。
2.2.2 equals方法
Object类中的equals方法用于检测一个对象是否等于另一个对象,该equals方法通过比较两个对象引用是否相等来判断对象是否相等,对于很多类来讲,这已经足够了,但是有时需要基于状态来判断,如果两个对象有相同的状态,也认为这两个对象相等。
例:两个员工的名字、薪水和雇佣日期一样,就认为他们是相等的。
public boolean equals(Object otherobjects){
if(this==otherobjects){
return true;
}
if(otherobjects==null) return false;
System.out.println(otherobjects.getClass());
System.out.println(this.getClass());
System.out.println(this.getClass()==otherobjects.getClass());
if(this.getClass()!=otherobjects.getClass()){
return false;
}
Employee temp=(Employee) otherobjects;
return Objects.equals(this.getName(),temp.getName())&&this.salary==temp.getSalary()&&Objects.equals(this.hairday,temp.hairday);
}
2.3 泛型数组列表
在c++中,如果使用数组,必须在编译时就确定数组的大小,而在java中,允许在运行时确定数组的大小
int actualSize=...
var staff = new Employee[actyalSize]
当然这段代码并没有完全解决解决运行时动态更改数组的问题,一旦确定了数组的大小,之后就很难对其进行更改,在java中可以使用ArrayList,ArrayList类似于数组,它可以自动调整数组的容量大小。
ArrayList是一个有类型参数的泛型类,需要指定数据类型,例:ArrayList<Employee>
2.3.1 声明数组列表
1、声明和构造一个保存Employee对象的数组列表
ArrayList<Employee> staff=new ArrayList<Employee>();
在java10中,可以使用var来避免重复使用变量名。
var staff =new ArrayList<Employee>();
如果没有用var可以省去后边的数据类型
ArrayList<Employee> staff=new ArrayList<>();
该原则称为菱形语法,注意:使用var不可以使用菱形语法
使用add方法将元素添加到数组列表中。
staff.add(new Employee("hhh",123));
数组列表管理着一个内部对象引用数组,最终这个数组的空间可能全部用尽,如果调用add时数组已经满了,数组列表会自动创建一个更大的数组,并将所有的对象拷贝过去。
如果已经可以估计数组可能的存储的元素数量,就可以在填充数组之前调用ensureCapacity方法,
staff.ensureCapacity(100);
该方法可以分配一个包含100个对象的数组,这样前100个就不用重新分配空间。
此外,还可以在声明时进行分配。
ArrayList<Employee> staff=new ArrayList<>(100);
一旦确定数组的大小保持恒定,不再发生变化,可以使用trimToSize方法,该方法将存储块的大小调整为保存当前元素数量所需要的空间,垃圾回收多余的空间。
2.3.2 访问数组列表元素
ArrayList类不能使用[]的语法进行访问或改变数组的元素,而是要使用get和set方法
例:要设置第i个元素
staff.set(i,harry);
等价于 a[i]=harry;
要得到一个数组列表的元素
Employee e=staff.get(i);
当没有泛型类时,原始的ArrayList类提供的get方法会返回Object,因此get方法的调用必须进行强制类型转换。 Employee e=(Employee) staff.get(i);
原始的ArrayList存在一定的危险性,add和set方法可以接收任何类型的对象。
staff.set(i,"harry");
他在正常编译时不会有任何报错,只有在检索对象并试图将它进行强制类型转换时,才会发现问题,如果使用ArrayList<Employee> 编译器就会检测这个错误。
2.4 对象包装器和自动装箱
有时,需要将int这样的基本类型转为对象,多余哦的基本类型都有一个对应的类,通常这些类称为包装器。
int --- Integer long--- Long float---Float
double--- Double short--Short char---Character bool --- Boolean
如果想要定义一个整形的数组列表,很遗憾,<>中的类型参数不允许是基本的数据类型,只可以写成,
var list =new ArrayList<Integer>(),如果向其中添加元素
list.add(1); 将自动转变为 list.add(Integer.valueOf(3));称为自动装箱
相反的,将一个Integer对象赋给一个int时也会自动拆箱,编译器会将
int n=list.get(i); 转换为 int n=list.get(i).intValue();
常用方法
int i=3;
Integer value= i;
System.out.println(value.intValue());
System.out.println(Integer.toString(4).getClass());
System.out.println(Integer.toString(20,2));
System.out.println(Integer.parseInt("50"));
System.out.println(Integer.parseInt("111",2));
System.out.println(Integer.valueOf("24"));
System.out.println(Integer.valueOf("111",2));
}
2.5 枚举类
public enum Size{Small,Medium,Large};
这是一个枚举类型,实际上,这个声明定义的类型是一个类,刚好有3个实例,不可以构造新的实例。因此当比较两个枚举类型的值时,不必调用equals,用“==”即可。
如果需要的话,也可以为枚举类添加构造器,方法和字段。当然构造器只是在构造枚举常量的时候使用,例:
enum Size{
Small("s"),medium("m"),Large("L");
private String abbreviation;
private Size(String abbreviation){
this.abbreviation=abbreviation;
}
public String getAbbreviation(){
return this.abbreviation;
}
}
枚举的构造器总是私有的所有的枚举类型都是Enum类的子类,继承了该类的许多方法。
如:
1、toString该方法会返回枚举常量名
Size.Small.toString();会返回字符串"Small"
2、toString 的所有逆方法都是静态方法valueof(static)
Size s=Enum.valueOf(Size.class,"Small")
将s设置为Size.Small
3、每个枚举类型都有一个静态的values方法,返回一个枚举类中的全部枚举值的一个数组
Size[] values=Size.values();
4、 ordinal方法返回enum声明枚举常量的位置,从0开始计数;
5、int compareTo(E other)
如果枚举常量出现在other之前,返回一个负整数,如果this==other返回0,否则,返回正整数。