Java学习总结之继承和多态

父类与子类

在Java术语中,如果C1类扩展自另一个类C2,我们称C2为父类,也称超类或基类,称C1为子类,也称次类、扩展类、派生类。父类是更通用的类,子类是扩充父类得到的更特定的类。子类从其父类继承所有成员(字段,方法和嵌套类),并可以添加新的字段和方法。构造函数不是成员,所以它们不被子类继承,但是可以从子类调用超类的构造函数。从物理层面来看,为子类对象开辟的内存的前一部分存储从父类继承的成员,后面再放置子类新扩展的成员。
注意:子类从其父类继承所有成员(字段,方法和嵌套类),不代表子类可以访问所有从父类继承的成员!

继承

面向对象的编程允许从已经存在的类中定义新的类,这称为继承。继承可以使得子类别具有父类别的各种属性和方法,而不需要再次编写相同的代码。在令子类别继承父类别的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类别的原有属性和方法,使其获得与父类别不同的功能。另外,为子类别追加新的属性和方法也是常见的做法。继承通过extends关键字实现。

继承有如下几个关键点

1.子类并不是父类的一个子集,实际上,一个子类通常比它的父类包含更多的信息和方法
2.父类中的私有数据域在该类之外是不可访问的,故不能在子类中直接使用,如果父类中定义了公共访问器或修改器,可以在子类中调用它们
3.继承是用来为"是一种"关系(is-a)建模的,不要仅仅为了重用方法这个原因而盲目使用继承
4.不是所有的"是一种"(is-a)关系都该用继承来建模
5.Java中类与类的继承是单一继承
6.继承应该遵循里氏替换原则,子类对象必须能够替换掉所有父类对象

super关键字

super关键字有两种作用:
1.调用父类的构造方法
因为继承时父类的构造方法不会被继承,而子类的构造方法只能初始化子类新增加的数据域,所以要通过super关键字调用父类的构造方法来初始化从父类继承的数据域。
形式是super()super(arguments),分别调用了父类的无参构造方法和相应的有参构造方法。super语句要写在子类构造方法的第一条,否则编译器会自动在构造方法开头插入一条super()

2.调用父类的方法
在继承时往往会进行重写,即覆盖父类的方法。此时如果要调用父类原有的未被覆盖的方法,就可以使用super.方法名来调用父类的方法。如果父类方法没有被覆盖,可以省略super关键字。只能调用一级父类的方法,不可以跨父类,super.super.方法名是不合法的。
有些人认为super与this引用在此处是类似的概念,实际上不太恰当,super不是一个对象的引用,也不能赋给另一个引用变量,他只是一个指示编译器调用超类方法的特殊关键字。

构造方法链

在任何情况下,构造一个类的实例时,将会调用沿着继承链的所有父类的构造方法。当构造一个子类对象时,子类构造方法会在完成自己的任务之前,首先调用它的父类的构造方法。如果父类继承自其他类,那么父类构造方法又会在完成自己的任务之前,调用它自己的父类的构造方法。这个过程一直持续到这个继承体系结构的最后一个构造方法被调用为止。这就是构造方法链。比如下面的代码:

public class Faculty extends Employee{
    public static void main(String[] args){
       new Faculty();
    }
    public Faculty(){
        System.out.println("(4) Performs Faculty's tasks");
    }
 }
class Employee extends Person{
    public Employee(){
        this("(2) Invoke Employee's overloaded constructor");
        System.out.println("(3) Perfoms Employee's tasks");
    }
    public Employee(String s){
      System.out.println(s);
    }
 }
 class Person{
    public Person() {
        System.out.println("(1) Performs Person's tasks");
     }
 }

打印结果为:
(1) Performs Person's tasks
(2) Invoke Employee's overloaded constructor
(3) Performs Employee's tasks
(4) Performs Faculty's tasks

我们可以知道:子类的构造方法的第一条语句要么是super语句(包括编译器隐式插入的),要么是this语句,它通过调用同一个类的另一个重载的构造方法,再调用该方法的super语句初始化父类

注意:如果没有显式在子类构造方法中定义super语句,编译器自动插入的super语句匹配的是父类的无参构造方法,如果父类没有无参构造方法,编译器会报错。所以建议为每个类都提供一个无参构造方法,以便于日后对该类进行扩展。

方法重载与重写

重载
方法重载是指在一个类中定义多个同名的方法,但要求每个方法具有不同的参数的类型或参数的个数。重载的方法方法名相同,返回值类型和访问修饰符可以不同,参数列表必须不同(包括参数个数、类型和次序,有一个不同就算不同)

重写
方法重写是指在继承时子类覆盖父类中已有的方法并提供对其的新的实现(修改方法体)。重写的方法和原方法方法名、参数列表均相同,可见性修饰符范围不可缩小(要么相同要么扩大),返回值类型可以是父类返回值类型或其子类型(称为协变返回类型)。这是为了确保可以使用父类实例的地方都可以使用子类实例,也就是确保满足里氏替换原则
为了避免错误,可以使用重写注解,在要重写的方法前加上@Override,该注解会强制编译器检查是否重写了某方法,如果没有重写,编译器会报告一个错误。

关于重写的几点注意
1.仅当实例方法是可访问的,它才能被覆盖
2.静态方法可以被继承,但不能被覆盖。如果子类中重新定义了父类的静态方法,父类的静态方法会被隐藏,可以使用父类名.静态方法名调用隐藏的静态方法
3.尽管重写一般用于方法,属性其实也可以重写,即子类可以定义和父类同名的属性,子类的属性会覆盖父类的属性。

方法重写发生在通过继承而相关的不同类中,方法重载可以发生在同一类中,也可以发生在由于继承关系而相关的不同类中。

多态

Java允许把子类对象的引用赋给父类引用变量,即父类型变量可以引用子类型的对象。多态就是指父类的具体实现可以呈现多种形态。从物理层面理解,继承自父类的子类的内存先存储父类数据域再存储子类的新增数据域,所以子类对象既可以看成子类对象本身(看整段内存)也可以看成父类对象(看内存的前一部分)。
注意:子类引用赋给父类变量之后,只能访问父类属性和方法或在子类中重写过的父类方法,不能访问子类的特有属性或方法,除非进行向下转型。这是因为编译器认为该对象是父类类型的,如果访问子类特有的属性或方法,将无法通过编译。

动态绑定

多态机制的底层实现技术是动态绑定,动态绑定是指JVM在执行期间(非编译期)判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。程序运行过程中,把函数(或过程)调用与响应调用所需要的代码相结合的过程称为动态绑定。

对java来说,绑定分为静态绑定和动态绑定;或者叫做前期绑定和后期绑定。

静态绑定(前期绑定):
在程序执行前方法已经被绑定(也就是说在编译过程中就已经知道这个方法到底是哪个类中的方法),此时由编译器或其它连接程序实现。例如:C 。针对java简单的可以理解为程序编译期的绑定;这里特别说明一点,java当中的方法只有final,static,private和构造方法是前期绑定。

动态绑定(后期绑定):在运行时根据具体对象的类型进行绑定。
若一种语言实现了后期绑定,同时必须提供一些机制,可在运行期间判断对象的类型,并分别调用适当的方法。也就是说,编译器此时依然不知道对象的类型,但方法调用机制能自己去调查,找到正确的方法主体。不同的语言对后期绑定的实现方法是有所区别的。但我们至少可以这样认为:它们都要在对象中安插某些特殊类型的信息。

简言之,一个方法可能在沿着继承链的多个类中实现,JVM在运行时动态绑定方法的实现。

理解方法调用

假设下面要调用x.f(args),方法调用的过程为:
1) 编译器查看对象的声明类型和方法名,可能存在多个名字为f的方法,比如f(int)和f(String)。编译器会一一列举类中所有名为f的方法和其超类中可访问的名为f的方法,至此编译器已获取所有可能被调用的候选方法。
2) 编译器将查看调用方法时提供的参数类型,如果所有f方法中存在一个与提供的参数类型完全匹配的方法,就选择这个方法,这个过程称为重载解析,如果没有完全匹配参数的方法,允许自动向上类型转换进行匹配,仍然没有合适的方法会报告编译错误。至此,编译器已获得需要调用的方法名字和参数类型。
3) 如果是private方法、static方法、final方法或者构造器,编译器可以准确知道应该调用哪个方法(即静态绑定),因为private方法、构造器不能被继承,更谈不上重写,而static方法和final方法虽然能被继承,但不能被重写。由于静态绑定,如果在子类中提供一个和父类中同名的静态方法,然后把子类引用赋给父类引用变量,调用该静态方法,实际调用的是父类的静态方法。

  1. 当程序运行,并且采用动态绑定调用方法时,JVM一定调用与x所引用对象的实际类型最合适的类的方法,即在沿着继承链从最特殊的子类开始向更通用的父类查找目标方法,直到找到对该方法的实现就停止,调用该方法。

每次调用方法都要进行搜索,时间开销很大。因此,JVM预先为每个类创建了一个方法表,其中列出了所有方法的签名和实际调用的方法。这样调用方法时JVM只要提取对象实际类型的方法表,搜索定义该方法签名的类,再调用该方法就可以了。
方法表示例(Manager继承自Employee):

Employee:
getName() -> Employee.getName()
getSalary() -> Employee.getSalary()
getHireDay() -> Employee.getHireDay()
raiseSalary(double) -> Employee.raiseSalary(double)

Manager:
getName() -> Employee.getName()//继承的方法
getSalary() -> Manager.getSalary()//重写的方法
getHireDay() -> Employee.getHireDay()//继承的方法
raiseSalary(double) -> Employee.raiseSalary(double)//继承的方法
setBonus(double) -> Manager.setBonus(double)//新增的方法

内联优化

带有final修饰符的类是不可派生的。在Java核心API中,有许多应用final的例子,例如java.lang.String,整个类都是 final的。为类指定final修饰符可以让类不可以被继承,为方法指定final修饰符可以让方法不可以被重写。如果指定了一个类为final,则该类所有的方法都是final的。Java编译器会寻找机会内联优化所有的final方法,内联对于提升Java运行效率作用重大,此举能够使性能平均提高50%。如果确定一个类不会被派生或一个方法不会被重写,建议使用final关键字修饰。

对象类型转换

和基本数据类型一样,对象可以自动进行向上转型(即多态),比如Apple类继承自Fruit类,把Apple对象的引用赋给Fruit变量一定是合法的。但如果向下转型(目的是访问子类特有数据域和调用子类特有方法),要进行强制类型转换,格式为:(子类名)对象名。此时要保证该对象的实际类型确实是要强制转换的子类型,比如fruit是一个引用了Apple对象的Fruit类型的变量,如果向下转型成Banana类是非法的,会抛出一个ClassCastException,而转成Apple类是合法的。
我们可以通过instanceof运算符来检测一个对象是否是某个类或接口的实例,其返回值是boolean类型的。
注意:
1.对象成员访问运算符(.)优先于类型转换运算符。使用圆括号保证在点运算符(.)之前进行转换,例如:((Circle)object).getArea();
2.转换基本类型值返回一个新的值,但转换一个对象引用不会创建一个新的对象。

Object类

Object类是Java中所有类的祖先(Java的类层次是一个单根结构),但不用显式写出public class xxx extends Object,在Java中只有基本数据类型不是对象,所有的数组类型,不管是对象数组还是基本类型数组都扩展了Object类。
下面介绍Object类中的几个重要方法及重写规范:

1) equals方法

Object类中的equals方法用于检测一个对象是否等于另外一个对象。在Object类中,这个方法将判断两个对象是否具有相同的引用,如果两个对象具有相同引用,它们一定是相等的。

equals方法的原型是public boolean equals(Object obj),默认实现是:

public boolean equals(Object obj){
    return (this == obj);
}

调用它的语法是object1.equals(object2),作用和直接使用==判等相同, 但这在大多数情况下没什么意义,我们一般通过判断对象的某些数据域是否相等来判断对象是否相等,此时我们需要重写equals方法。
比如类Employee定义了数据域:private String name,private double salary,private LocalDate hireDay
equals方法重写如下:

public boolean equals(Object obj){
    if(this == obj) return true;
    //快速检测引用是否相等,相等返回ture
    
    if(obj == null) return false;
    //检测引用是否为空,为空返回false  
    
    if(getClass() != obj.getClass())  
        return false;
     //检测是否属于同一个类,不是返回false  
        
    Empolyee other = (Employee) obj;//向下转型  
    
    return name.equals(other.name)
      && salary == other.salary
      && hireDay.equals(other.hireDay);
      //逐一比较数据域,有一个不等返回就false,否则返回true

进一步改进:
改进一
上述的第4步检测,可以改为

return Objects.equals(name,other.name)
   && salary == other.salary
   && Objects.equals(hireDay,other.hireDay);

其中Objects.equals方法可以防备name 或 hireDay 可能为null的情况,如果两个参数都为null,Objects(a,b)返回true;如果其中一个为null,返回false;如果两个参数都不为null,调用a.equals(b)。Objects类在java.util包中。

在子类中定义equals方法时,首先调用超类的equals,如果检测失败,对象就不可能相等。如果超类中的域相等,只需要比较子类中新增的数据域就可以。
比如Manager类继承自Employee,在父类的基础上增加了private double bonus:

    public boolean equals(Object obj){
        if(!super.equals(obj)) return false;
        Manager other = (Manager) obj;
        return bonus == other.bonus;
    }

改进二
上述代码的第3步使用了getClass检测,这适用于子类拥有自己相等概念的情况,比如雇员和经理,只要对应的姓名、薪水和雇佣日期相等,而奖金不相等,就认为是不同的,可以使用getCalss检测。但是如果超类决定相等的概念,就可以使用instanceof进行检测,比如雇员的ID作为相等的概念,就可以用xxx instanceof Employee进行检测,并将Empolyee.equals声明为final。

equals方法要满足下面的特性

  1. 自反性: 对于任何非空引用,x.equals(x)应该返回true
  2. 对称性:对于任何引用x和y, x.equals(y)的结果应该和y.equals(x)的结果相同
  3. 传递性:对于任何引用x、y和z,如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)也应该返回true
  4. 一致性: 如果x和y引用的对象没有发生变化,反复调用x.equals(y)应该返回同样的结果
  5. 对于任何非空引用x,x.equals(null)应该返回false

下面我们给出编写一个完美的equals方法的建议

  1. 先快速检测引用是否相等,如果相等两个对象一定相等,不用继续检测
  2. 检测引用是否为空,如果为空,不必再检测,直接返回不等
  3. 如果equals语义在每个子类中有所改变(子类决定相等的概念),用getClass检测:if(getClass() != obj.getClass()) return false;如果所有子类都拥有统一的语义(父类决定相等),就使用instanceof检测:if(!(obj instanceof ClassName) return false)
  4. 将obj向下转型为相应类的类型变量
  5. 逐一比较数据域,注意基本数据类型用 == 检测,引用类型用Objects.equals()方法检测
    数组对象用静态的Arrays.equals()方法判等

拓展:Integer判断相等,到底该用==还是equals

当我们在方法里调用equals方法时,建议写字符串常量.equals(参数)的形式。比如:

public static boolean function(String str){
    return "hello world".equals(str);
}

这样可以防止str为null而抛出空指针异常,而根据equals()方法的对称性,这种调换并不会影响方法的功能。

2) hashCode方法

散列码(hash code)是由对象导出的一个整型值。散列码是没有规律的,hashCode() 返回散列码,而 equals() 是用来判断两个对象是否等价。等价的两个对象散列码一定相同,但是散列码相同的两个对象不一定等价。
在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证等价的两个对象散列值也相等。
String类使用下列算法计算散列码:

int hash = 0;
for(int i = 0;i < length();i++){
    hash = 31 * hash + charAt(i);
}

由于hashCode方法定义在Object类中,方法原型是public int hashCode(),因此每个对象都有一个默认的散列码,其值为对象的存储地址。内容相同的字符串的散列码相同,因为字符串的散列码是由内容导出的,而内容相同的StringBuilder对象的散列码不同,因为StringBuilder类中没有重写hashCode方法,它的散列码是Object类定义的默认hashCode方法导出的对象存储地址。
hashCode方法应该返回一个整型数值(可以是负数),并合理地组合实例域的散列码,以便能让各个不同的对象产生的散列码更均匀。
例如,下面是Employee类的hashCode方法

public int hashCode(){
    return 7 * name.hashCode()
      + 11 * new Double(salary).hashCode()
      + 13 * hireDay.hashCode();

进一步改进

public int hashCode(){
    return 7 * Objects.hashCode(name)
      + 11 * Double.hashCode(salary)
      + 13 * Objects.hashCode(hireDay);

从上面的代码我们可以发现,在重写hashCode方法时,我们要充分组合类的实例域,其中引用类型用Objects.hashCode(),基本类型用相应包装类的hashCode()
其中Objects.hashCode()是null安全的,如果参数为null,返回值是0,否则返回调用参数调用hashCode的结果。另外,使用静态方法Double.hashCode来避免创建Double对象

还有更简单的做法

public int hashCode(){
    return Objects.hash(name,salary,hireDay);
 }

Equals和hashCode的定义必须一致,如果X.equals(y)返回true,那么x.hashCode()就应该等于y.hashCode(),用于组合散列的实例域equals中用于比较的实例域,比如equals比较的是雇员的ID,就需要散列ID
如果重写了equals方法,就必须重写hashCode方法,以便用户可以将对象插入到散列表中。
下面的代码中,新建了两个等价的对象,并将它们添加到 HashSet 中。我们希望将这两个对象当成一样的,只在集合中添加一个对象,但是因为 EqualExample 没有实现 hasCode() 方法,因此这两个对象的散列值是不同的,最终导致集合添加了两个等价的对象。

EqualExample e1 = new EqualExample(1, 1, 1);
EqualExample e2 = new EqualExample(1, 1, 1);
System.out.println(e1.equals(e2)); // true
HashSet<EqualExample> set = new HashSet<>();
set.add(e1);
set.add(e2);
System.out.println(set.size());   // 2

必读:搞懂 Java equals 和 hashCode 方法

3) toString方法

方法原型是public String toString(),用于返回一个描述当前对象的字符串,Object类提供的默认实现是返回一个形式为:类名@对象十六进制内存地址的字符串,我们需要重写toString方法来返回更清晰的描述。数组对象则返回一个类似[I@1a46e30的字符串(前缀[I表明是一个整型数组,@后面是数组对象的十六进制内存地址),修正方法是调用Arrays.toString方法,如果是多维数组需要调用Arrays.deepToString方法。
toString不仅可以显式调用,也会在需要一个描述对象的字符串时隐式调用,比如用System.out.println语句打印一个对象,相当于打印对象的toString方法返回的字符串;又比如通过操作符"+"连接字符串时连接一个对象会调用它的toString方法。
当重写toString时,如果返回的字符串涉及到类名,不要硬加入类名,可以通过getClass().getName()获得类名字符串,这样子类如果要调用父类的toString只用super.toString()就可以得到带有父类名的完整字符串描述。

补充:Object类的九个方法

泛型数组列表

在许多程序设计语言中,特别是在C++中,必须在编译时就确定整个数组的大小。而在Java中,允许在运行时确定数组大小。比如:

int actualSize = . . .;
Employee[] staff = new Employee[actualSize];

但这还是没有完全解决运行时动态更改数组的问题,于是我们引入了一个ArrayList类(在Java.util包中),它使用起来有点像数组,但在添加或删除元素时具有自动调节数组容量的功能。
ArrayList是一个采用类型参数泛型类,声明方式为ArrayList<E> arrayList = new ArrayList<E>(),从Java SE 7开始,可以省去右边的类型参数,即ArrayList<E> arrayList = new ArrayList<>()
在Java SE 5以前的版本没有提供泛型类,只有一个ArrayList类,保存Object类型的元素,等价于ArrayList<Object>,可放入任何类型的对象。其提供的get方法只能返回Object对象,需要进行强制类型转换;且add方法和set方法不检查参数类型,具有一定的危险性。
ArrayList管理这对象引用的一个内部数组,如果数组满了,ArrayList会自动创建一个更大的数组,并将所有的对象从较小的数组拷贝到较大的数组中。

ArrayList类的常用API

  • ArrayList<E>() 构造一个初始容量为10的空列表
  • ArrayList<E>(int initialCapacity)构造一个具有指定初始容量的空列表
  • boolean add(E e) 将指定的元素添加到此列表的尾部,永远返回true
  • void add(int index, E element) 将指定的元素插入此列表中的指定位置。
  • int size() 返回此列表中的元素数。
  • void ensureCapacity(int minCapacity) 如果可以预先确定要插入的元素个数,使用此方法一次性扩容到位,否则通过add方法需要多次拷贝扩容,大大降低效率。
  • E get(int index) 返回此列表中指定位置上的元素。
  • E remove(int index) 移除此列表中指定位置上的元素并返回该元素。
  • boolean remove(Object o) 移除此列表中首次出现的指定元素(如果存在)。
  • void set(int index, E element) 用指定的元素替代此列表中指定位置上的元素。
  • boolean isEmpty() 如果此列表中没有元素,则返回 true
  • boolean contains(Object o) 如果此列表中包含指定的元素,则返回 true。
  • void clear() 移除此列表中的所有元素。
  • Object clone() 返回此 ArrayList 实例的浅表副本。
  • trimToSize() 将此 ArrayList 实例的容量调整为列表的当前大小。

对于数组列表有用的方法

1.数组和ArrayList互相转换

数组转ArrayList:

String[] array = {"red","green","blue"};
ArrayList<String> list = new ArrayList<>(Arrays.asList(array));

陷阱1:public static <T> List<T> asList(T... a)方法体现的是适配器模式,只是转换接口,后台的数据仍是数组。返回的是Arrays 类的一个私有的静态内部类,并不是我们通常所熟悉的java.util包下的ArrayList。该内部类直接继承AbstractList<E> ,不支持add和remove等修改集合的方法,而是直接继承AbstractList<E>中的add和remove等方法,调用就会直接抛出java.lang.UnsupportedOperationException。所以,改进方法是用一个ArrayList对其进行包装,即new ArrayList<>(Arrays.asList(array))
陷阱2:public static <T> List<T> asList(T... a)方法不适用于基本数据类型

ArrayList转数组:

String[] array = new String[list.size()];
list.toArray(array);

比较: toArray()方法有两个重载方法,分别为:list.toArray()和list.toArray(T[] a)。对于第一个重载方法,是将list直接转为Object[]数组(注意不能对Object[]类型的结果用(T [])进行强制转换,会报错!)第二种方法是将list转化为你所需要类型的数组。
两个方法简单总结:
1、toArray()方法无需传入参数,可以直接将集合转换成Object数组进行返回,而且也只能返回Object类型。
2、toArray(T[] a)方法需要传入一个数组作为参数,并通过泛型T的方式定义参数,所返回的数组类型就是调用集合的泛型,所以自己无需再转型,但是根据传入数组的长度 与集合的实际长度的关系,会有不同的处理:
* a:数组长度不小于集合长度,直接拷贝,不会产生新的数组对象;
* b:数组长度小于集合长度,会创建一个与集合长度相同的新数组,将集合的数据拷贝到新数组并将新数组的引用返回。

2.char数组和String互相转换
String转char[]:

String s = "12321323";
char[] ss = s.toCharArray();

char[]转String:

char[] ss = s.toCharArray();
String.valueOf(ss);

3.Collections类
Collections类中有很多适用于ArrayList的静态方法,比如max和min方法返回列表中的最大和最小元素,sort方法对列表排序,shuffle方法随机打乱列表元素。

ArrayList的元素只能是非基本数据类型的,所以如果要容纳基本类型的元素,我们需使用相应的包装类,且此时ArrayList的使用支持自动装箱和自动开箱。

final关键字总结

final关键字在不同语句中有不同的作用:
1.修饰类变量或成员变量 如果是基本数据类型,表示该变量的值不可改变;如果是引用类型,表示该变量不能再指向其它对象,即引用值不可变,但是被引用的对象本身是可以修改的。只能在定义时、构造方法中或构造代码块中赋值。
2.修饰方法的局部变量,即常量 final修饰的局部变量只要在使用前被赋值即可,不要求在定义时即赋值,但赋值后不可改变。
3.修饰方法 表示该方法不能被重写,注意final不能修饰构造方法。
4.修饰类 表示该类不能被继承

继承的设计技巧

1.将公共操作和域放在超类

2.不要使用受保护的域

protected机制并不能够带来更好的保护,其原因主要有两点:
第一,子类集合是无限制的,任何一个人都能够由某个类派生一个子类,并编写代码以直接访问protected的域,从而破坏了封装性。
第二,同一个包中的所有类都可以访问protected的域,而不管它是否为这个类的子类。
不过,protected方法对于指示那些不提供一般用途而应在子类中重新定义的方法很有用,比如Object提供的clone方法就是protected的

3.使用继承实现 "is-a" 关系

4.除非所有继承的方法都有意义,否则不要使用继承

比如在使用ArrayList类设计一个Stack类时,不应该使用继承,因为栈只能在栈顶操作元素,而ArrayList中的方法可以在任意位置插入、删除和访问任意位置的元素,这显然不合适。此时应该使用组合代替继承。

5.在覆盖方法时,不要改变预期的行为

6.使用多态,而非类型信息

即尽量面向父类泛化编程,把不同子类的类似行为的方法定义在父类里,并在子类里覆盖该行为

7.不要过多地使用反射

反射机制使得人们可以通过在运行时查看域和方法,让人们编写出更具有通用性的程序。这种功能对于编写系统程序极为实用,但不适于编写应用程序。反射是很脆弱的,即编译器很难帮助人们发现程序中的错误,因此只有在运行时才发现错误并导致异常。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,293评论 18 399
  • (一)Java部分 1、列举出JAVA中6个比较常用的包【天威诚信面试题】 【参考答案】 java.lang;ja...
    独云阅读 6,937评论 0 62
  • 静静的河中间, 飘着一个小纸船, 没有舵也没有帆。 它缓缓向前, 任水阔路远, 没有拘束也没有负担。 水快它就快,...
    水到渠成1阅读 609评论 4 19
  • 重拾余华写的《活着》内心很是触动,这篇文章有感而发仿此书局部。写出人对苦难的承受能力,对世界乐观的态度。人是为活着...
    偶喵喵喵喵啊阅读 607评论 0 0
  • 炎炎的夏日,虽然炎热但却让人心生怜爱,期待已久的暑假终于来到。虽然不用朝九晚五的上班,但是生活似乎更加忙碌,...
    慧angle阅读 428评论 0 5