Java基础之继承

继承
this关键字的用途

    引用隐式参数;
    调用该类其他的构造器。

super关键字的用途

    调用超类的方法。
    调用超类的构造器。

多态

可以通过 is-a规则,它表明子类的每个对象也是超类的对象。例如,每个
经理都是雇员。因此,将Manager类设计为Employee类的子类是显而易见的,反之不然,并不是每一名雇员都是经理。

is-a 规则的另一种表述法是置换法则。它表明程序中出现超类对象的任何地方都可以用子类对象置换。
例如:可以将一个子类的对象赋给超类变量

Employee e;
e=new Employee();
e=new Manager();



理解方法调用

弄清楚如何在对象上应用方法调用非常重要。下面假设要调用 x.f(args),隐式参数x声明为类C的一个对象。下面是调用过程的详细描述:
1. 编译器查看对象的声明类型和方法名。

假设调用 x.f(args),且隐式参数x声明为C类的对象。需要注意的是:有可能存在多个名字为f,但参数类型不一样的方法。例如,可能存在
方法 f(int)和方法f(String)。编译器将会一一列举所有C类中名为f的方法和其超类中访问属性为public且名为f的方法。(超类的私有方法不可访问)
2. 编译器将查看调用方法时提供的参数类型。

如果在所有名为f的方法中存在一个与提供的参数类型完全匹配,就选择这个方法。这个过程称为**重载解析**。例如,对于调用 `x.f("HelloWorld")`
来说,编译器将会挑选 `f(String)`,而不是 `f(int)` 。由于允许类型转换(int 可以转换成double,Manager可以转换成Employee,等等),所以这个过程
可能很复杂。如果编译器没有找到与参数类型匹配的方法,或者发现经过类型转换后有多个方法与之匹配,就会报告一个错误。

至此,编译器已获得需要调用的方法名字和参数类型。

**方法的名字和参数列表称为方法的签名,返回类型不是签名的一部分。


3. 如果是Private方法、static方法、final方法或者构造器,那么编译器将可以准确地知道应该调用哪个方法,我们将这种调用方法称为静态绑定。与此对应的是,
调用的方法依赖于隐式参数的实际类型,并且在运行时实现动态绑定。在我们列举的示例中,编译器采用动态绑定的方式生成一条调用 f(String) 的指令。

    当程序运行,并且采用动态绑定调用方法时,虚拟机一定调用与x所引用对象的实际类型最合适的那个类的方法。

    假设x的实际类型是D,它是C类的子类。如果D类定义了方法 f(String),就直接调用它;否则,将在D类的超类中寻找 f(String),

    每次调用方法都要进行搜索,时间开销相当大。因此,虚拟机预先为每个类创建了一个方法表(method table),其中列出了所有方法的签名和实际调用的方法。这样一来。
    在真正调用方法的时候,虚拟机仅查找这个表就行了。在前面的例子中。虚拟机搜索D类的方法表,以便寻找与调用 f(String) 相匹配的方法。这个方法既有可能是 D.f(String),
    也有可能是 X.f(String),这里的X是D的超类。这里需要提醒一点,如果调用 super.f(param),编译器将对隐式超类的方法表进行搜索。

    现在,查看一下程序清单5-1中调用e.getSalary() 的详细过程。

    动态绑定有一个非常重要的特性:无需对现存的代码进行修改,就可以对程序进行扩展。假设增加一个新类 Executive,并且变量e有可能引用这个类的对象,我们不需要
    对包含调用 e.getSalary()的代码进行重新编译。如果e恰好引用一个Executive类的对象,就会自动地调用 Executive.getSalary()方法。

阻止继承:final类和方法

不允许扩展的类称为final类。

 public final class Executive extends Manager{
    ...
 }

equal方法

Object类中的equals方法用于检测一个对象是否等于另外一个对象。**在Object类中,这个方法将判断两个对象是否具有相同的引用。如果
两个对象具有相同的引用,它们一定是相等的**。从这点上看,将其作为默认操作也是合乎情理的。

Java 语言规范要求equals方法具有下面的特性:
1. 自反性:对于任何非空引用x,x.equals(x) 应该返回true;
2. 对称性:对于任何引用x和y,当且仅当y.equals(x)返回true,x.equals(y)
也应该返回true。
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方法的建议:

    显式参数命名为 otherObject,稍后需要将它转换成另一个叫做other的变量。

    检测this与otherObject是否引用同一个对象:

    if(this==otherObject) return true;

    检测otherObject是否为null,如果为null,返回false。

    if(otherObject==null) return false

    比较this与otherObject是否属于同一个类。如果euqals的语义在每个子类中有所改变,就使用
    getClass 检测:

    if(getClass()!=otherObject.getClass()) return false
    将otherObject 转换为相应的类类型变量:
    ClassName other=(ClassName)otherObject
    例如: TODO 添加例子:



hashCode 方法

散列码是由对象导出的一个整型值。散列码是没有规律的。
字符串(String)的散列码是由内容导出的。
字符串缓存(StringBuffer)类中没有定义hashCode方法,它的散列码是由Object
类的默认hashCode方法导出的对象存储地址。
toString方法

设计子类的model,POJO时必须重写toString方法。并将子类域的描述添加进去。
toString 方法是一种非常有用的调试工具。在标准类库中,许多类都定义了toString方法。
以便用户能够获得一些有关对象状态的必要信息。
对象包装器与自动装箱

所有的基本类型都有一个与之对应的类。这些类称为包装器。这些对象包装器类拥有很明显的名字:
Integer,Long,Float,Double,Short,Byte,Character,Void和Boolean(前6个类派生于公共的超类Number)。对象
包装器类是不可变的,即一旦构造了包装器,就不允许更改包装器在其中的值。同时,对象包装器类还是final,因此不能定义它们的子类。

自动装箱

例如:在集合ArrayList中,下面的调用 list.add(3) 将自动地变换成 list.add(Integer.valueOf(3))
这种变换被称为自动装箱。

自动拆箱

当将一个Integer对象赋给一个int值时,将会自动地拆箱。也就是说,编译器将下列语句:
int n=list.get(i);
翻译成
int n=list.get(i).intValue()

大多数情况下,容易有一种假象,即基本类型与它们的对象包装器是一样的。只是它们的相等性不同。大家知道,
== 运算符也可以应用于对象包装器对象。只不过检测的是对象是否指向同一个存储区域,因此,下面的比较通常不会成立:

 Integer a=1000;
 Integer b=1000;
 if(a==b) ...


然而,Java实现却有可能让它成立。如果经常出现的值包装到同一个对象中,这种比较就有可能成立。这种不确定的结果的结果
并不是我们所希望的。解决这个问题的办法是在两个包装器对象比较时调用equals 方法。

自动装箱规范要求boolean,byte,char<=127,介于-127~127之间的short和int被包装到固定的对象中。例如,如果前面的例子中将a和b
初始化为100,对他们进行比较的结果一定成立。

如果在一个条件表达式中混合使用Integer和Double类型,Integer值就会被拆箱,提升为double,再装箱为Double:

Integer n=1;
Double x=2.0;
System.out.println(true?n:x);  //Prints 1.0


装箱和拆箱是编译器认可的。而不是虚拟机。编译器在生成类的字节码时,插入必要的方法调用。虚拟机只是执行这些字节码。

字符串转成整型。
int x=Integer.parseInt(s);

Java方法都是值传递的。如下所示:

    public static void main(String[] args) {
        int x=3;
        triple(x);
        System.out.println("计算后的x="+x);
    }

    public static void triple(int x) {
        x = 3 * x;
        System.out.println("经过运算的x="+x);
    }


输出结果是:经过运算的x=9,计算后的x=3

例如:

    public static void main(String[] args) {
          Employee outEmployee = new Employee();
          outEmployee.setName("测试");
          call(outEmployee);
          System.out.println("employee:"+outEmployee.getName());
      }

      public static void call(Employee inEmployee) {
          Employee employee = new Employee();
          employee.setName("cba");
          inEmployee.setName("abc");
          inEmployee = employee;
      }



输出结果是:employee:abc
分析:在此程序中,这次调用等于声明了两个变量outEmployee和inEmployee,他们指向的是同一个地址,
调用call方法。只是将outEmployee的地址传给了inEmployee,而outEmployee本身并没有传递过去(也就是引用没有传递过去)
所以在再次赋值时只是影响了inEmployee。
这样的传递方式只能称为址传递,或者是引用对象传递,而不能说是引用传递。




作者:码农飞哥
微信公众号:码农飞哥