JAVA 继承
访问权限
可以使用super来访问超类的public属性或者protect属性。
子类不能使用超类的私有对象和私有方法。
super:
this是当前对象的引用,super只是一个指示编译器去调用超类方法的关键字,不能把super作为参数传递。
使用super调用超类构造器的语句,必须是子类构造器的第一条语句。
没有子类构造器没有显示的调用超类的构造器,那么java将默认调用超类的无参构造器,如果这是超类没有无参构造器,那么会编译报错。如下:
也就是说,子类的数据是继承自超类的,java要先初始化超类,才能有子类,所以必须在构造子类之前完成超类的构造。
关于继承和数组的一个困惑
-
package com.zjf;
-
-
import com.zjf.spring.mybatis.model.Person;
-
-
public class Test {
-
public static void main(String[] args) {
-
//定义一个Women数组
-
Women[] w = new Women[3];
-
//因为Women继承自Person 所以可以这样赋值
-
//这个时候,编译器只知道p是Person[] 但是运行的时候,其实它是一个Women[]
-
Person[] p = w;
-
//因为编译器知道p是Person[] 所以这么操作可以编译通过 但是运行的时候 就会报错了
-
p[0] = new Person();
-
}
-
}
结果:
Exception in thread "main" java.lang.ArrayStoreException: com.zjf.spring.mybatis.model.Person
at com.zjf.Test.main(Test.java:11)
理解这个问题 要理解编译时和运行时的概念。这种情况只会是数组才有,因为如果是一个对象,父对象可以执行的操作,子对象都可以执行,但是父数组可以添加Person,子数组却是不行的。
方法覆盖
方法名和参数构成了方法签名,方法的返回值不是。
在覆盖超类的方法时,如果方法签名要一致,那么返回值只能是超类的返回类型或者它的子类。
其实在覆盖整个层面,可以理解成方法签名要把返回值类型也加上,试想如果返回值可以不一样,那么在超类调用整个方法,根本不知道会返回什么类型。
再覆盖超类方法的时候,子类方法不能低于超类方法的可见性(public private 包访问权限)。
静态绑定和动态绑定:
程序绑定的概念:
绑定指的是一个方法的调用与方法所在的类(方法主体)关联起来。对java来说,绑定分为静态绑定和动态绑定;或者叫做前期绑定和后期绑定.
静态绑定:
在程序执行前方法已经被绑定(也就是说在编译过程中就已经知道这个方法到底是哪个类中的方法),此时由编译器或其它连接程序实现。例如:C。
针对java简单的可以理解为程序编译期的绑定;这里特别说明一点,java当中的方法只有final,static,private和构造方法是前期绑定
所以static方法不能被重写,但是可以被继承。编译器可以去超类寻找静态方法。
动态绑定:
后期绑定:在运行时根据具体对象的类型进行绑定。
若一种语言实现了后期绑定,同时必须提供一些机制,可在运行期间判断对象的类型,并分别调用适当的方法。也就是说,编译器此时依然不知道对象的类型,但方法调用机制能自己去调查,找到正确的方法主体。不同的语言对后期绑定的实现方法是有所区别的。但我们至少可以这样认为:它们都要在对象中安插某些特殊类型的信息。
而动态绑定的典型发生在父类和子类的转换声明之下:
比如:Parent p = new Children();
其具体过程细节如下:
1:编译器检查对象的声明类型和方法名。
假设我们调用x.f(args)方法,并且x已经被声明为C类的对象,那么编译器会列举出C 类中所有的名称为f 的方法和从C 类的超类继承过来的f 方法。
2:接下来编译器检查方法调用中提供的参数类型。
如果在所有名称为f 的方法中有一个参数类型和调用提供的参数类型最为匹配,那么就调用这个方法,这个过程叫做"重载解析"。
3:当程序运行并且使用动态绑定调用方法时,虚拟机必须调用同x所指向的对象的实际类型相匹配的方法版本。
假设实际类型为D(C的子类),如果D类定义了f(String)那么该方法被调用,否则就在D的超类中搜寻方法f(String),依次类推。
JAVA 虚拟机调用一个类方法时(静态方法),它会基于对象引用的类型(通常在编译时可知)来选择所调用的方法。相反,当虚拟机调用一个实例方法时,它会基于对象实际的类型(只能在运行时得知)来选择所调用的方法,这就是动态绑定,是多态的一种。动态绑定为解决实际的业务问题提供了很大的灵活性,是一种非常优美的机制。
前面的理论当中已经提到了java的绑定规则,由此可知,在处理java类中的成员变量时,并不是采用运行时绑定,而是一般意义上的静态绑定。所以在向上转型的情况下,对象的方法可以找到子类,而对象的属性还是父类的属性。
代码如下:
-
public class Father {
-
-
protected String name="父亲属性";
-
-
public void method() {
-
System.out.println("父类方法,对象类型:" + this.getClass());
-
}
-
}
-
-
public class Son extends Father {
-
protected String name="儿子属性";
-
-
public void method() {
-
System.out.println("子类方法,对象类型:" + this.getClass());
-
}
-
-
public static void main(String[] args) {
-
Father sample = new Son();//向上转型
-
System.out.println("调用的成员:"+sample.name);
-
}
-
}
结论,调用的成员为父亲的属性。
这个结果表明,子类的对象(由父类的引用handle)调用到的是父类的成员变量。所以必须明确,运行时(动态)绑定针对的范畴只是对象的方法。
现在试图调用子类的成员变量name,该怎么做?最简单的办法是将该成员变量封装成方法getter形式。
-
public class Father {
-
protected String name = "父亲属性";
-
public String getName() {
-
return name;
-
}
-
public void method() {
-
System.out.println("父类方法,对象类型:" + this.getClass());
-
}
-
}
-
-
public class Son extends Father {
-
protected String name="儿子属性";
-
-
public String getName() {
-
return name;
-
}
-
-
public void method() {
-
System.out.println("子类方法,对象类型:" + this.getClass());
-
}
-
-
public static void main(String[] args) {
-
Father sample = new Son();//向上转型
-
System.out.println("调用的成员:"+sample.getName());
-
}
-
}
结果:调用的是儿子的属性
阻止继承:
使用final,可以作用在类和方法上。
类型判断:
-
package com.zjf;
-
-
-
public class Test {
-
public static void main(String[] args) {
-
Women w = new Women();
-
//true
-
System.out.println(w instanceof Person);
-
Person p = new Person();
-
System.out.println(w.getClass() == p.getClass());
-
//不会报空指针 而是flase
-
System.out.println(null instanceof Person);
-
}
-
}
结果:
true
false
false
数组对象
java中除了基本类型(数值 字符 布尔型)之外都是对象,包括数组。
所以数组也是继承自Object对象。
-
package com.zjf;
-
-
-
public class Test {
-
public static void main(String[] args) {
-
Person[] p = new Person[3];
-
Object obj = p;
-
obj = new Person();
-
}
-
}
上面代码没有错误。
相等测试
quals方法的重要性毋须多言,只要你想比较的两个对象不仅仅局限在只是同一对象(即它们的引用相同),你就应该实现equals方法,让对象用你认为相等的条件来进行比较。
Java语言规范要求equals拥有以下特性:
1。自反性:对于任何引用类型, o.equals(o) == true成立。
2。对称性:如果 o.equals(o1) == true 成立,那么o1.equals(o)==true也一定要成立。
3。传递性:如果 o.equals(o1) == true 成立且 o1.equals(o2) == true 成立,那么o.equals(o2) == true 也成立。
4。一致性:如果第一次调用o.equals(o1) == true成立再o和o1没有改变的情况下以后的任何次调用都成立。
5。o.equals(null) == true 任何时间都不成立。
编写一个完美的equals方法的建议:
给出以下编写建议:
2、检测this与oherObject是否引用同一个对象:
if (this == otherObject) return true;
这条语句只是一个优化。实际上,这是一种经常采用的形式。因为计算这个等式要比一个一个地比较类中的域所付出的代价小得多。
3、检测otherObject是否为null,如果为null,返回false。这项检测是很有必要的。
If(otherObject == null)return false;
4、比较this与otherObject是否属于同一个类。如果equals的语义在每个子类中有所改变,就使用getClass检测:
if(getClass != otherObject.get Class)return false;
如果所有的子类都拥有统一的语义,就使用instanceof检测:
if(!(otherObject instanceof ClassName)) return false;
5、将otherObject转换成相应的类类型变量:
ClassName other = (ClassName) othrerObject
6、现在开始对所有需要比较的域进行比较。使用==比较基本类型域。使用equals比较对象域。如果所有的域都匹配,就返回true;否则返回false。
如果在子类中重新定义equals,就要在其中包含调用super.equals(other)。(这里不是强制要求的,而是作者觉得这样比较好)
提示:对于数组类型的域,可以使用静态的Arrays.equals方法检测相应的数组元素是否想等。
我的疑惑:
主要在第四条:
感觉不能说所有子类有统一的语义,就要使用instanceof检测。
如果所有子类都有统一的语义,如都是通过ID字段来对比,我们也可以使用getClass来比较,因为public方法是动态绑定的,如果所有子类都没有重写equals,所有子类最终都会使用超类的equals,也就是getClass来比较,然后使用ID比较。这样的话,这样的话,可以实现代码的复用。只是不允许不同类型的子类交叉比较,但是同一类型的子类比较是符合逻辑的。
所以重点的是,是否允许交叉比较。即使子类的语义不一样,但是主要比较的一部分可以共用超类,那么超类使用getClass也是正确的,因为子类可以使用super.equals把超类的逻辑拿下来。
我的验证:
-
package com.zjf;
-
-
public class Person {
-
-
public Person(Integer id) {
-
this.id = id;
-
}
-
private Integer id;
-
-
public boolean equals(Object objObject) {
-
if(objObject == null)
-
{
-
return false;
-
}
-
//如果不是严格同一类型 那么久不相等
-
if(this.getClass() != objObject.getClass())
-
{
-
return false;
-
}
-
Person other = (Person)objObject;
-
return this.id.equals(other.id);
-
}
-
-
}
-
-
-
-
-
-
package com.zjf;
-
-
-
public class Women extends Person {
-
-
public Women(Integer id,String name) {
-
super(id);
-
this.name = name;
-
}
-
private String name;
-
-
@Override
-
public boolean equals(Object obj) {
-
if(!super.equals(obj))
-
{
-
return false;
-
}
-
//如果能往下走 证明肯定obj肯定是Women类型 而且id属性已经匹配成功了 我们可以继续往下加属性
-
Women other = (Women)obj;
-
return this.name.equals(other.name);
-
}
-
}
-
-
-
-
-
package com.zjf;
-
-
public class Men extends Person {
-
-
public Men(Integer id,String name) {
-
super(id);
-
this.name = name;
-
}
-
private String name;
-
-
@Override
-
public boolean equals(Object obj) {
-
if(!super.equals(obj))
-
{
-
return false;
-
}
-
//如果能往下走 证明肯定obj肯定是Men类型 而且id属性已经匹配成功了 我们可以继续往下加属性
-
Men other = (Men)obj;
-
return this.name.equals(other.name);
-
}
-
-
}
-
-
-
-
-
package com.zjf;
-
-
-
public class Test {
-
public static void main(String[] args) {
-
Person w = new Women(1,"xhj");
-
Person m = new Men(1,"zjf");
-
System.out.println(w.equals(m));
-
Person w1 = new Women(1,"xhj");
-
System.out.println(w.equals(w1));
-
}
-
}
打印结果:
false
true
接着我们把,getclass换成instanceof ,然后重新执行,然后:
Exception in thread "main" java.lang.ClassCastException: com.zjf.Men cannot be cast to com.zjf.Women
at com.zjf.Women.equals(Women.java:19)
at com.zjf.Test.main(Test.java:8)
报错了,很尴尬。。
原因很简单,如果想交叉比较,那么比较方法中不能出现非超类的属性。也就是说不能子类就不要重写equals了,因为你要交叉比较,逻辑肯定个都要按照超类的来。
所以,如果使用了instanceof 子类就不要重写equals了。
注:equals方法的参数是Object类型的,如果换位如Person类型,不生效。使用@override可以检测是否是重写超类的方法。
hashCode方法
散列码(hash code)是由对象导出的一个整型值.散列码是没有规律的.如果x和y是两个不同的对象,x.hashCode()和y.hashCode()基本不会相同.
hashCode是Object对象的方法,因此每个对象都有默认的实现,根据内存中的地址生成。
String类使用下列算法计算散列码:
-
int hash = 0;
-
for (int i = 0; i < length(); i++)
-
hash = 31 * hash + charAt(i);
而StringBuffer和StringBuilder使用Object的默认实现。
equals与hashCode的定义必须一致:如果x.equals(y)返回 true,那么x.hashCode()必须与y.hashCode()具有相同的值(反过来没有要求,这是有Hash表的实现原理而导致的限制,所以两个不同的字符串,它的hashCode是可以重复的)。如果重新定义equals方法,就必须重新定义hashCode方法,以便用户可以将对象插入到散列表中.
hashCode方法应该返回一个整型数值,并合理地组合实例域的散列码,以便能够让各个不同的对象产生的散列码更加均匀.
可以使用Arrays.hashCode来生成数组元素的hashCode。
ArrayList
ArrayList的容量:
每个 ArrayList 实例都有一个容量。该容量是指用来存储列表元素的数组的大小。它总是至少等于列表的大小。随着向 ArrayList 中不断添加元素,其容量也自动增长。并未指定增长策略的细节,因为这不只是添加元素会带来分摊固定时间开销那样简单。
注:容量不能限时ArrayList的元素数量。只是为了优化的目的。
ArrayList() |
|
ArrayList(int initialCapacity) |
|
void |
ensureCapacity(int minCapacity) |
void |
trimToSize() |
泛型与原始数组列表的兼容性:
java为了实现没有泛型之前的数组的兼容性,有以下规范:
- 一个接受List<Person>类型参数的方法,可以接受List类型。
- 一个接受List类型参数的方法,可以接受List<Person>类型。
- 但是,一个接受List<Person>参数的方法,不可以接受List<Man>,虽然Person和Man是继承关系,但是加上List却不是。
基本类型包装器
泛型不支持基本类型,如没有ArrayList<int>。只能是ArrayList<Integer>,自动打包int。
Integer Character Float等包装类型都是不可变对象,跟String的机制一样,实现了缓存共享,所以不能用==比较。
枚举
在第3章已经讲过如何定义枚举类型,下面是一个典型的例子:
public enum Size {SMALL, MEDIUM, LARGE, EXTRA_LARGE};
实际上,这个声明定义的类型是一个类,它刚好有4个实例,在此尽量不要构造新对象.
因此,在比较两个枚举类型的值,不需要调用eqauls,而直接使用"=="就可以了.
如果需要的话,可以在枚举类型中添加一些构造器,方法和域.
所有的枚举类型都是Enum类的子类.它们继承了这个类的许多方法,其中最有用的一个是toString,这个方法能够返回枚举常量名.例如,Size.SMALL.toString()将返回字符串"SMALL".
toString的逆方法是静态方法valueOf,例如,语句:
size s = Enum.valueOf(size.class, "SMALL");
将s设置成Size.SMALL .
每个枚举类型都有一个静态的values方法,它将返回一个包含全部枚举值的数组.例如,如下调用
Size[] values = Size.values();
返回包含元素Size.SMALL,...,Size.EXTRA_LARGE的数组.
ordinal方法返回 enum 声明中枚举常量的位置(从0开始),例如,Size.MEDIUM.ordinal()返回1 .
程序5-12中的EnumTest.Java如下所示:
-
package enums;
-
-
import java.util.Scanner;
-
-
public class EnumTest
-
{
-
public static void main(String[] args)
-
{
-
Scanner 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("ordinal = " + size.ordinal());
-
System.out.println("abbreviation = " + size.getAbbreviation());
-
if (size == Size.EXTRA_LARGE)
-
System.out.println("Good job--you paid attention to the ");
-
}
-
}
-
enum Size
-
{
-
SMALL("S"), MEDIUM("M"), LARGE("L"), EXTRA_LARGE("XL");
-
-
private Size(String abbreviation) { this.abbreviation = abbreviation; }
-
public String getAbbreviation() { return abbreviation; }
-
private String abbreviation;
-
}