继承的含义
继承已存在的类就是复用(继承)这些类的方法和域,在此基础上,还可以添加一些新的方法和域.is-a关系是继承的一个明显特征
类,超类,子类
例如
public class Manager entends Employee
{
//添加方法和域
}
关键词entends表明正在构造的新类派生于一个已存在的类
- 已存在的类称为超类,基类或父类
- 新类称为子类,孩子类,派生类
- 子类往往比超类拥有更丰富的域和方法
- 在扩展子类的时候,仅需要指出子类与超类的不同之处,而将通用的方法放在超类中
覆盖方法
当超类中的方法子类并不适用的时候
例如
//超类
class Employee{
//加薪
private double salary;
public double getSalary(){
return salary;
}
}
//子类
public class Manager entends Employee
{
private double bouns = 200;
public double getSalary(){
//返回奖金总和
double baseSalary = super.getSalary();
return bouns+baseSalary;
}
}
具体来说,子类中的getSalary()方法需要返回奖金总和,尽管Manger类也有一个salary域,但是Manger类的getSalary()方法并不能直接访问Employee类的Salary域.只有Employee才能访问他自己的私有域,如果Manger类有需求访问,需要借助Employee类的接口,就是getSalary()方法,我们需要调用超类的方法而不是子类,所以需要加上surpe关键字.
子类构造器
public class Manager entends Employee
{
public Manger(String name,double salary,int year,int month,int day)
{
surpe(name,salary,year,month,day);
bonus = 0;
}
}
由于子类的构造器不能访问超类的私有变量,所以必须利用超类的构造器对这部分私有域进行初始化,我们可以通过surpe实现对包含这些参数的构造器显式的调用,否则将会默认使用超类的无参构造,如果既没有子类的显式调用,超类又没有写无参构造,就会报告错误
多态与动态绑定
一个对象变量可以指示多种实际类型的现象称为多态
在运行时能够自动选择执行哪个方法的现象称为动态绑定
在java中,默认的处理方式就是动态绑定
事例
//超类
package inheritance;
import java.time.LocalDate;
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;
}
}
//子类
package inheritance;
public class Manager extends Employee {
private double bouns;
public Manager(String name,double salary,int year,int month,int day){
super(name,salary,year,month,day);
bouns = 0;
}
public double getSalary(){
double baseSalary = super.getSalary();
return baseSalary +bouns;
}
public void setBouns( double bouns){
this.bouns = bouns;
}
}
//main方法
package inheritance;
public class ManagerTest {
public static void main(String[] args) {
Manager boss = new Manager("nike",90000,2000,10,10);
boss.setBouns(10000);
Employee[] staff = new Employee[3];
staff[0] = boss;
staff[1] = new Employee("make",40000,1989,12,15);
staff[2] = new Employee("tom",30000,1990,12,15);
for(Employee e:staff){
e.raiseSalary(5);
}
for (Employee e:staff){
System.out.println("name"+e.getName()+" "+"salary"+e.getSalary()+" "+"hireDay"+e.getHireDay());
}
}
}
继承层次
含义:
由一个超类派生出所有子类的集合称为继承层次
继承链
含义:
由某个特定的类到其祖先的路径被称为该类的继承链
- java中不支持多继承
- 相关的多继承应该通过接口实现
多态
判断是否应该设计成继承的简单规则"is-a"
它表明子类的每个对象也可以是超类的对象
例如
Employee e;
e = new Employee(...);
e = new Manager(...);
置换法则的优点
staff[0] = boss;
staff[1] = new Employee("make",40000,1989,12,15);
staff[2] = new Employee("tom",30000,1990,12,15);
但是不能将超类的引用赋给子类变量
例如,下面的引用是非法的
Manager m = staff[i];
//因为不是所有的员工都是经理
还需要注意的是,虽然子类数组的引用可以转化为超类数组的引用,但是所有的数组都要牢记创建他们时候的元素类型,否则就像下面的例子
staff[0] = new Employee(...);
//很显然将普通员工添加到了经理当中,但下面他将找不到setbouns方法,会发生运行时错误,
//如果试图存储不兼容的数组类型将会发生ArrayStoryException异常
理解方法调用
方法调用的一般步骤
-
编辑器查看对象的声明类型和方法名
-
然后编辑器将从java虚拟机方法表中列出所有可用的方法(java虚拟机预先为每个类提供了一个方法表)
-
接下来编辑器查看参数类型(一旦参数类型完全匹配,就选择这个方法,这个过程叫做重载解析)
-
最后虚拟机调用方法
例如在Emoployee的方法表中,列出了这个类的所有方法 Employee: getName() ->Employee.getName() getSalary() ->Employee.getSalary() getHireDay()->Employee.getHireDay() raiseSalary() ->Employee.raiseSalary() 实际上这并不完整,省略了Object超类的方法
动态绑定的特性:无需对现存的代码进行修改,就可以对程序进行扩展
阻止继承
不允许被扩展(继承)的类称为final类
final类需要提前声明
例如
public final class Manager extends Employee {
}
//类中的特定方法也可以声明为final方法,这将意味着他不能被覆盖重写
强制类型转换
有时候需要将某个类的对象装换为另一个类来使用
//对象引用的语法与类数值表达式的类型转换相似
例如
Manager boos = (Manager)staff[0];
进行类型转换的唯一原因是:在暂时忽视对象的实际类型之后,使用对象的全部功能
注意:
-
java编译器允许将子类的引用赋给超类,但将一个超类的引用赋给子类变量必须进行类型转换
-
如果试图在继承链上进行向下的转换,并且谎报有关对象包含的内容,会产生ClassCastException异常
-
使用instanceof操作符可以进行类型转换前的检查
例如
if(staff[1] instanceof Manager){}
综上所述
- 只能在继承层次类内进行对象引用的类型转换
- 在超类转换成子类之前,应该用instanceof进行检查
- 在一般情况下,推荐不使用类型转化
抽象类
在java程序设计中,抽象是一个十分重要的概念
抽象类的简单概念
-
使用abstract关键字定义的类
-
如果一个类包含一个或多个抽象方法就必须被定义为抽象类
-
即使某个类不包含任何抽象方法,也可以根据需要被定义为抽象类
-
抽象类不能被实例化,但可以创建一个具体子类的对象
实例 package abstractClass; //定义一个抽象类 public abstract class Person { //定义一个抽象方法 public abstract String getDescription(); private String name; //构造器 public Person(String name){ this.name = name; } public String getName(){ return name; } }
//抽象类的子类
package abstractClass;
import java.time.LocalDate;
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;
hireDay = LocalDate.of(year,month,day);
}
public double getSalary(){
return salary;
}
public LocalDate getHireDay(){
return hireDay;
}
//重写抽象类中的描述方法
public String getDescription() {
return String.format("an employee with a salary of $%.2f",salary);
}
public void raiseSalary(double byPercent){
double raise = salary * byPercent / 100;
salary += raise;
}
}
package abstractClass;
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;
}
}
package abstractClass;
public class PersonTest {
public static void main(String[] args) {
Person[] people = new Person[2];
//java当中的多态与动态绑定
people[0] = new Employee("tom",10000,2000,11,9);
people[1] = new Student("jake","computer");
for(Person p: people){
System.out.println(p.getName()+""+p.getDescription());
}
//对象的类型转换,超类的引用转化成子类
Employee staff = (Employee)people[0];
//类型转换的意义,忽略对象类型使用全部方法
staff.raiseSalary(5);
//得到上涨后的薪资
System.out.println(staff.getSalary());
}
}
理解这个例子就理解了抽象类
受保护访问(Protected)
- 如果希望超类中的某些方法或者某些域允许被子类访问,就可以把这些方法或者这些域设置成Protected属性
- 但是建议受保护属性最好应用在方法而不是域上,因为这违背了oop的数据封装原则
归纳java用于控制可见性的四个修饰符
1.仅对本类可见--------------privite
2.被所有类可见--------------publice
3.对本包和所有子类可见------protected
4.对本包可见----------------默认(什么都不写)
Object类:所有类的超类
在java中,所有类都是Object类扩展开来的,熟悉这个类提供的所有服务特别重要
equals方法
Object类中的equals方法用于检测一个对象是否等于另一个对象.
- 如果两个对象具有相同的引用,他们一定是相等的
相等测试与继承
java语言规范要求equsls具有以下特性
- 自反性:对于任何非空引用x,x.equals(x)应该返回true,x.equals(y)也应该返回true.
- 对称性:对于任何引用x,y 当且仅当x.equals(y)为true时,y.equals(x)也应该为true
- 传递性:对于任何引用x,y,z,如果x.equals(y)返回true,y.equals(z)返回true,x.equals(z)也应该返回true.
- 一致性:如果x和y的引用的对象没有发生变化,反复调用x.equals(y)应该返回一致的结果
- 对于任意非空引用x,x.equals(null)应该返回false
重点:在java类库中拥有超过150个equals方法的具体实现,问题来了,java中为什么要重写equals方法?
- Object类中equals方法比较的是两个对象的引用地址,只有对象的引用地址指向同一个地址时,才认为这两个地址是相等的,否则这两个对象就不相等。
- 如果有两个对象,他们的属性是相同的,但是地址不同,这样使用equals()比较得出的结果是不相等的,而我们需要的是这两个对象相等,因此默认的equals()方法是不符合我们的要求的,这个时候我们就需要对equals()方法进行重写以满足我们的预期结果。
- 在java的集合框架中需要用到equals()方法进行查找对象,如果集合中存放的是自定义类型,并且没有重写equals()方法,则会调用Object父类中的equals()方法按照地址比较,往往会出现错误的结果,此时我们应该根据业务需求重写equals()方法。
下面是一个给出编写完美equals方法的步骤建议
1.显式参数命名为otherObject,稍后需要将它转化为另一个叫做other的变量
2.检测this和object类是否引用同一个对象
if(this == otherObject) return ture;
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 false;
5.将otherObject转换为相应的类和变量
ClassName other = (ClassName) otherObject;
6.现在开始对所有需要比较的域进行比较,使用==比较基本域,使用equals比较对象域,如果所有的域都匹配,则返回true,否则返回false
如果在子类中重新定义equals,就要在其中包含super.equals(other).
对于数组类型的域,可以用静态方法Array.equals()进行比较
hashcode方法
散列码(hashcode)是由对象导出的一个整型值,散列码没有规律,每个对象都有一个默认的散列码,其值为对象的储存地址.
例如
String s = "OK";
StringBuilder sb = new StringBuilder(s);
System.out.println(s.hashCode() + " " + sb.hashCode());
String t = "OK";
StringBuilder tb = new StringBuilder(t);
System.out.println(t.hashCode() + " " + tb.hashCode());
输出的结果是
2524 460141958
2524 1163157884
字符串s,t拥有相同的散列码,而字符串缓冲sb和tb却拥有着不同的散列码,这是因为StringBuffer类中并没有定义hashcode方法,它的散列码就是Object类默认的导出对象的存储地址
所以,重新定义equals方法,就需要重新hashcode方法,以便用户可以将对象插入散列表
因为如果x.equals(y)返回true,那么x.hashcode()就必须与y.hashcode()返回相同的散列码.
首先,最好使用null安全的方法Object.hashcode()
public int hashcode()
{
//如果参数为null,则将返回0
return 7*Object.hashcode(name)
//使用静态方法Double.hashcode来避免创建double对象
+11*Double.hashcode(salary)
+13*Object.hashcode(hireday);
}
还有更好的方式,如果需要组合多个参数的散列值,可以使用Objects.hash()方法
public int hashcode()
{
return Objects.hash(name,salary,hireday);
}
toString 方法
-
toString方法用于返回表示对象值的字符串
-
只要对象与一个字符串通过+号连接起来,java就会默认调用toString方法
-
强烈建议为自定义的每个类添加一个toString方法
例如
public String toString() { return getClass().getName() +"[name="+name +",salary"+salary +",hireday"+hireday +"]"; }
例外的是,如果想打印出数组,应该使用Array.toString()静态方法,多维数组应该使用Arrays.deepToString()方法
泛型数组列表
在java中,运行在运行时动态更改数组的大小
例如
Int actualSize = ...;
Employee[] staff = new Employee[actualSize] ;
但是这样并不能实际的解决问题,java提供了一个被称为ArrayList的类,它使用起来比较像数组,但在添加或删除元素时,具有自动调节大小的功能
ArrayList是一个采用类型参数的泛型类
声明和构造一个Employee对象
ArrayList<Employee> staff = new ArrayList<>;
//使用add方法可以将元素添加到数组列表中
staff.add(new Employee("tom",1000,...));
//size方法将返回实际的数组数量
staff.size();
访问数组列表元素
数组列表自动扩展容量的便利增加了访问元素的复杂程度,原因是ArrayList并不是java程序语言的一部分,而是某个人写的实用类
-
使用get和set方法实现对数组元素的访问和修改
//get方法的调用者必须对返回值进行类型转换,怒人就会返回Object类型
Employee e = (Employee)staff.get(i);
//尽量使用add方法而不是set,后者会把元素替换掉
staff.set(i,harry); -
下面的一个技巧可以一举两得
首先创建一个数组,并添加所有的元素
ArrayList<X> list = newArrayList(); while(....) { X = ....; list.add(X); }
然后用toArray方法将它拷贝到另一个元素中
X[] a = new X[list.size()] list.toArray(a);
可以在数据列表尾部或者中间插入新元素
-
对数组实施插入删除的效率比较低,对于小型数组来说,这一点不必担心,但如果数组存储的元素太多,就需要用到链表.
对象包装器与自动装箱
所有的基本类型都有一个与之对应的类,这些类称为包装器
例如
Integer.Long.Float.Double.Shot.Byte.Character.Void.Boolean
这六个类派生于公共的超类Number
假设一个整形数组列表,由于尖括号内的数组类型不允许是基本类型,所以应该是
ArrayList<Integer> list = new ArrayList<>;
从而有一个自动装箱的特性就是
list.add(3);
//将自动转换为
list.add(Integer.valueOf(3));
相反把Integer类型的数据插入进int类型时,会自动拆箱
这些特性提供便利的同时,如果插入的值为null,则会报告NullPointException异常
有个十分明显的使用,就是将数值转化放在包装器中
int x = Integer.parseInt(s);
参数数量可变的方法(变参)
现在版本的java提供了可变的参数调用的方法
除了printf这样的方法外,用户可以自定义可变参数的方法,并将参数指定为任意类型,甚至是基本类型
例如 计算某些数值的最大值
public static double max(double... values){
double largest = Double.NEGATIVE_INFINITY;
for(double v:values)
if(v>largest)
largest = v;
return largest;
}
public static void main(String[] args) {
double m = max(3.1,40.4,5);
//编译器将new double[] {3.1,40.4,-5}传给方法
System.out.println(m);
}
枚举类
所有的枚举类型都是Eunm类的子类,他们继承了许多这个类的方法
其中最有用的是toString()方法,这个方法返回枚举常量名
toString方法的逆方法是valueOf(),给变量设置常量名
下面是一个典型的例子
枚举类判断衣服尺码
import java.util.Scanner;
public class EnumTest {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
System.out.println("Enter size:(SMALL,MEDIUM,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.LARGE) {
System.out.println("you are boy");
}
}
enum Size {
SMALL("S"), MEDIUM("M"), LARGE("L");
private String abbreviation;
private Size(String abbreviation) {
this.abbreviation = abbreviation;
}
public String getAbbreviation() {
return abbreviation;
}
}
}
除反射外,继承方面告一段落