Java笔记(继承)

定义子类

可以如下继承Employee类来定义Manager类,这里用关键字extends表示继承。
public class Manager extends Employee;
{
  added methods and fields
}
在Manager类中,增加了一个用于存储奖金信息的域,以及一个用于设置这个域的新方法:
public class Manager extends Employee
{
  private double bonus;
  ...
  public void setBonus(double bonus)
  {
    this.bonus = bonus;
  }
}
这里定义的方法和域没区别,若有一个Manager对象,就可以使用setBonus方法。
Manager boss = ...;
boss.setBonus(5000);
  

覆盖方法
然而,超类中的有些方法对子类Manager并不一定适用,具体来说,Manager类中的getSalary方法应该返回薪水和奖金的总和。为此,需要提供一个新的方法来覆盖(override)超类中的这个方法:

public class Manager extends Employee
{
  ...
  public double getSalary()
  {
    ...
  }
  ...
}
因为Manager类的getSalary方法不能够直接地访问超类的私有域。所以Manager类的方法一定要访问私有域,就必须借助于公有的接口,Employee类中的公有方法getSalary正是这样一个接口。
将对salary域的访问替换成调用getSalary方法。
public double getSalary()
{
  double baseSalary = getSalary();
  return baseSalary + bonus;
}
以上还是不行,是因为Manager类也有一个getSalary方法,可使用关键字super解决:
super.getSalary()
最后解决如下:
public double getSalary()
{
  double baseSalary = super.getSalary();
  return baseSalary + bonus;
}

注: super不是一个对象的引用,不能将super赋给另一个对象变量,它只是一个指示编译器调用超类方法的特殊关键字。

子类构造器

public Manager(String name,double salary ,int year ,int month,int day)
{
  super(name, salary, year, month, day);
  bonus = 0;
}
这里的关键字super具有不同的含义,语句super(n,s,year,month,day);
是”调用超类Employee中含有n,s,year,month,day参数的构造器“的简写形式。

由于Manager类的构造器不能访问Employee类的私有域,所以必须利用Employee类的构造器对这部分私有域进行初始化,我们可以通过super实现对超类构造器的调用。使用super调用构造器的语句必须是子类构造器的第一条语句。

关键字this有两个用途:一是引用隐式参数,二是调用该类其他的构造器。
super也有两个用途:一是调用超类的方法,二是调用超类的构造器。

例:创建一个新经理,并设置他的奖金
Manager boss = new Manager("Carl Cracker",80000,1987,12,15);
boss.setBonus(5000);
下面定义一个包含3个雇员的数组:
Employee[] staff = new Employee[3];
将经理和雇员都放都数组中:
staff[0] = boss; 
staff[1] = new Employee("Harry Hacker",50000,1989,10,1);
staff[2] = new Employee("Tony Tester",40000,1990,3,15);
输出每个人的薪水:
for(Employee e: staff)
    System.out.println(e.getName()+""+e.getSalary());//将e声明为Employee类型
当e引用Employee对象时,e.getSalary()调用的是Employee类中的getSalary方法;当e引用Manager对象时,e.getSalary()调用的是Manager类中的getSalary方法。

一个对象变量(例如e)可以指示多种实际类型的现象被称为多态(polymorphism)。在运行时能够自动地选择调用哪个方法的现象称为多态绑定(dynamic binding)。

继承层次
继承并不仅限于一个层次。由一个公共超类派生出来的所有类的集合被称为继承层次(inheritance hierarchy)。在继承层次中,从某个特定的类到其祖先的路径被称为该类的继承链(inheritance chain)。(Java不支持多继承)

多态: “is-a”规则,它表明子类的每个对象也是超类的对象。例如,每个经理都是雇员,因此将Manager类设计为Employee类的子类是显而易见的。is-a的另一种表述法是置换法则。

例如,可将一个子类的对象赋给超类变量:
Employee e;
e = new Employee(...);//Employee object expected
e = new Manager(...);//ok,Manager can be used as well

在Java中,子类数组的引用可以转换成超类数组的引用,而不需要采用强制类型的转换。例如:
Manager[] managers = new Manager[10];
将它转换成Employee[]数组完全是合法的:
Employee[] staff = managers;

方法的名字和参数列表称为方法的签名。
不过,返回类型不是签名的一部分,因此,再覆盖方法时,一定要保证返回类型的兼容性。允许子类将覆盖方法的返回类型定义为原返回类型的子类型。例如,假设Employee类有

public Employee getBuddy(){...}
经理不会想找这种地位低下的员工,为了反映这一点,在后面的子类Manager中,可以按照如下所示的方式覆盖这个方法
public Manager getBuddy(){...}
我们说,这两个getBuddy方法具有可协变的返回类型。

注:在覆盖一个方法的时候,子类方法不能低于超类方法的可见性。特别是,如果超类方法是public,子类方法一定要声明为public。经常会放生这类错误:在声明子类方法的时候,遗漏了public修饰符。此时,编译器将会把它解释为试图提供更严格的访问权限。

阻止继承:final类和方法
不允许扩展的类被称为final类。如果在定义类的时候使用了final修饰符就表明这个类是final类。

格式如下:
public final class Executive extends Manager
{
  ...
}
类中的特定方法也可以被声明为final。如下:
public class Employee
{
  ...
  public final String getName()
  {
    return name;
  }
  ...
}
//域也可被声明为final,对于final域来说,构造对象之后就不允许改变它们的值。不过,若一个类声明为final,只有其中的方法自动地称为final,而不包括域。

将方法或类声明为final主要目的是:确保它们不会再子类中改变语义。例如,Calendar类中的getTime和setTime方法都声明为final。如果有一个String的引用,他引用的一定是一个String对象,而不可能是其他类的对象。
注: 将一个子类的引用赋给一个超类变量,编译器是允许的。但将一个超类的引用赋给一个子类变量,必须进行类型转换,这样才能通过运行时的检查。

在进行类型转换之前,先查看一个是否能成功地转换,可使用instanceof操作符如:

if(staff[1] instanceof Manager)
{
  boss = (Manager) staff[1];
}

综述:只能在继承层次内进行类型转换;在将超类转换成子类之前,应该使用instanceof进行检查。

抽象类
类即使不含抽象方法,也可以将类声明为抽象类。
抽象类不能被实例化。也就是说,如果将一个类声明为abstract,就不能创建这个类的对象。

需要注意,可以定义一个抽象类的对象变量,但是它只能引用非抽象子类的对象。例如:
Person p = new Student(“Vince Vu”,“Economics”);

下面定义一个扩展抽象类Person的具体子类Student:
public class Student extends Person
{
  pirvate String major;

  public Student(String name,String major)
  {
    super(name);
    this.major = major;
  }
  public String getDescription()
  {
   return "a student majoring in" + major;
  }
}
在Student类中定义了getDexcription方法。因此,在Student类中的全部方法都是非抽象的,这个类不再是抽象类。

受保护访问
Java中,一般将类中的域标记为private,而方法标记为public。任何声明为private的内容对其它类都是不可见的。
然而,在有些时候,人们希望超类中的某些方法允许被子类访问,或允许子类的方法访问超类的某个域,为此需要将这些方法或域声明为protected。例如,若将超类Employee中的hireDay声明为protected,而不是私有的,Manager中的方法就可以直接地访问它。

不过,Manager类中的方法只能够访问Manager对象中的hireDay域,而不能访问其他Employee对象中的这个域。

归纳Java用于控制可见性的4个访问修饰符
1.仅对本类可见——private;
2.对所有类可见——public;
3.对本包和所有子类可见——protected;
4.对本包可见——默认,不需要修饰符。

Object类:所有类的超类

可以使用Object类型的变量引用任何类型的对象:
Object obj = new Employee("Harry Hacker",35000);

在Java中,只有基本类型(primitive types)不是对象,例如,数值,字符和布尔类型的值都不是对象。
所有的数组类型,不管是对象数组还是基本类型的数组都扩展了Object类。

Employee[] staff = new Employee[10];
obj = staff;
obj = new int[10];

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

public class Employee
{
  ...
  public boolean equals(Object otherObject)
  {
   if(this == otherObject) return true;
   if(otherObject == null) return false;
   if(getClass()!=otherObject.getClass())
     return false;
     Employee other = (Employee) otherObject;
     return name.equals (other.name)
     &&salary == other.salary
     &&hireDay.equals(other.hireDay);
   }
}
为了防备name或hireDay可能为null的情况,需要使用Object.equals方法。如果两个参数都为null,Object.equals(a,b)调用将返回true;如果其中一个参数为null,则返回false;否则,如果两个参数都不为null,则调用a.equals(b)。利用这个方法,Employee.equals方法的最后一条语句要改写为:
return Objects.equals(name,other.name)
  &&salary == other.salary
  &&Object.equals(hireDay,other.hireDay);
在子类中定位equals方法时,首先调用超类的equals。如果检测失败,对象就不可能相等。如果超类中的域都相等,就需要比较子类中的实例域。
public class Manager extends Employee
{
  ...
  public boolean equals(Object otherObject)
  {
  if(!super.equals(otherObject)) return false;
  //super.equals checked that this and otherObject belong to the same class
  Manager other = (Manager)otherObject;
  return bonus == other.bonus;
  }
} 

如果由超类决定相等的概念,那么就可以使用instanceof进行检测,这样可以在不同子类的对象之间进行相等的比较。
下面给出编写一个完美的quals方法的建议
1.显式参数命名为otherObject,稍后需要将它转换成另一个叫做other的变量。
2.检测this与otherObject是否引用同一个对象:

if(this == otherObject) return true;

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

if(otherObject == null) return false;

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

if(getClass() != otherObject.getClass()) return false;
如果所有的子类的子类都拥有统一的语义,就使用instanceof检测:
if(!(otherObject instanceof ClassName)) return false;

5.将otherObject转换为相应的类类型变量:
ClassName other = (ClassName) otherObject
6.现在开始对所有需要比较的域进行比较了。使用==比较基本类型域,使用equals比较对象域。

return field == other.field1
  &&Object.euqals(field2,other.field2)
  &&...;

对于数组类型的域,可以使用静态的Arrays.equals方法检测相应的数组元素是否相等。

hashCode方法
散列码(hashCode)是由对象导出的一个整型值。散列码是没有规律的。如果x和y是两个不同的对象,x.hashCode( )与y.hashCode( )基本上不会相同。
由于hashCode方法定义在Object类中,因此每个对象都有一个默认的散列码,其值为对象的存储地址。如下:

String s = "ok";
StringBuilder sb = new StringBuilder(s);
System.out.println(s.hashCode()+" "+sb.hashCode());
String t = new String("ok");
StringBuilder tb = new StringBuilder(t);
System.out.println(t.hashCode()+" "+tb.hashCode());

如果重新定义equals方法,就必须重新定义hashCode方法,以便用户可以将对象插入到散列表中。
toString方法
用于返回表示对象值的字符串。

下面是Employee类中的toString方法的实现:
public String toString()
{
  return"Employee[name="+name+",salary="+salary+",hireDay="+hireDay+"]";
}
如果超类使用了getClass( ).getName( ),那么子类只要调用super.toString( )就可以了。例如:
public class Manager extends Employee
{
  ...
  public String toString()
  {
    return super.toString()
    +"[bonus="+bonus+"]};
  }
}
如果x是任意一个对象,并调用System.out.println(x);
println方法就会直接地调用x.toString( ),并打印输出得到的字符串。

泛型数组列表
ArrayList是一个采用类型参数(type parameter)的泛型类(generic class)。它使用起来有点像数组,但在添加或删除元素时,具有自动调节数组容量的功能,而不需要为此编写任何代码。

下面声明和构造一个保存Employee对象的数组列表:
ArrayList<Employee>staff = new ArrayList<Employee>();
也可:ArrayList<Employee>staff = new ArrayList<>();
使用add方法可以将元素添加到数组列表中。如下,展示了如何将雇员对象添加到数组列表中的方法:
staff.add(new Employee("Harry Hacker",...));
staff.add(new Employee("Tony Tester",...));
如果调用add且内部数组已经满了,数组列表就将自动地创建一个更大的数组,并将所有的对象从较小的数组中拷贝到较大的数组中。
如果已经清楚或估计出数组可能存储的元素数量,就可以在填充数组之前调用ensureCapacity方法:
staff.ensureCapacity(100);
这个方法调用将分配一个包含100个对象的内部数组。然后调用100次ad,而不用重新分配空间。
另外,还可以把初始容量传递给ArrayList构造器:
ArrayList<Employee>staff = new ArrayList<>(100);

一旦能够确认数组列表的大小不再发生变化,就可以调用trimToSize方法。这个方法将存储区域的大小调整为当前元素数量所需要的存储空间数目。垃圾回收器将回收多余的存储空间。
一旦整理了数组列表的大小,添加新元素就需要花时间再次移动存储块,所以应该在确认不会添加任何元素时,再调用trimToSize。

若数组存储的元素数比较多,又经常需要在中间位置插入、删除元素,就应该考虑使用链表。
对象包装器与自动装箱
有时,需要将int这样的基本类型转换为对象。所有的基本类型都有一个与之对应的类。
例如,integer类对应基本类型int。通常,这些类称为包装器(wrapper)。

假设想定义一个整型数组列表。而尖括号中的类型参数不允许是基本类型,也就是说,不允许写出ArrayList。这里就用到了Integer对象包装器类。我们可以声明一个Integer对象的数组列表。

ArrayList<Integer> list = new ArrayList<>();
幸运的是,有一个很有用的特性,从而更加便于添加int类型的元素到ArrayList<Integer>中。下面这个调用:
list.add(3);
将自动地变换成:
list.add(Integer.valueOf(3));这种变换被称为自动装箱(autoboxing)。
相反地,当将一个Integer对象赋给一个int值时,将会自动地拆箱。也就是说,编译器将下列语句:
int n = list.get(i);
翻译成
int n = list.get(i).intValue();

如果在一个条件表达式中混合使用Integer和Double类型,Integer值就会拆箱,提升为double,再装箱为Double:
Integer n =1;
Double x=2.0;
System.out.println(true ? n:x);
最后强调一下,装箱和拆箱是编译器认可的,而不是虚拟机。编译器在生成类的字节码时,插入必要的方法调用。虚拟机只是执行这些字节码。

枚举类

public enum Size {SMALL,MEDIUM, LARGE, EXTRA_LARGE};//定义的类型是一个类
例:
public enum Size
{
  SMALL("S"),MEDIUM("M"),LARGE("L"),EXTRA_LARGE("XL");

   private String abbreviation;

   private Size(String abbreviation){ this.abbreviation = abbreviation; }
   public String (String abbreviation(){return abbreviation;}
}
所有的枚举类型都是Enum类的子类。它们继承了这个类的许多方法。其中最有用的一个是toString,这个方法能够返回枚举常量名。例如:Size.SMALL.toString( )将返回字符串“SMALL"。
toString的逆方法是静态方法valueOf。例如:
Size s = Enum.valueOf(Size,class,"SMALL");//将s设置成Size.SMALL。
每个枚举类型都有一个静态的values方法,它将返回一个包含全部枚举值的数组。
Size[] valuse = Size.values( );
odinal方法返回enum声明中枚举常量的位置,位置从0开始计数。
例如:Size.MEDIUM.ordinal()返回I。

反射
反射库(reflection library)提供了一个非常丰富且精心设计的工具集,以便编写能够动态操纵Java代码的程序。这项功能被大量应用于JavaBeans中,它是Java组体的体系结构。特别是在设计或运行中添加新类时,能够快速地应用开发工具动态地查询新添加类的能力。
能够分析类能力的程序称为反射(reflective)。反射机制的功能极其强大,可以用来:
1.在运行时分析类的能力;
2.在运行时查看对象,例如,编写一个toString方法供所有类使用;
3.实现通用的数组操作代码;
4.利用Method对象,这个对象很像C++中的函数指针。

Class类
Java运行时系统始终为所有的对象维护一个被称为运行时的类型标识。

如同用一个Employee对象表示一个特定的雇员属性一样,一个Class对象将表示一个特定类的属性。最常用的Class方法时getName。例如:
System.out.println(e.getClass().getName()+" "+e.getName());
如果e是一个雇员,则会打印输出:
Employee Harry Hacker
如果e是经理,则会打印输出:
Manager Harry Hacker
如果类在一个包里,包的名字也作为类名的一部分:
Random generator = new Random();
Class cl = generator.getClass();
Strinig name = cl.getName();
还可以调用静态方法forName获得类名对应的Class对象。
String className = "java.util.Random";
Class cl = Class.forName(className);

获得Class类对象的第三种方法非常简单。如果T是任意的Java类型(或void关键字),T.class将代表匹配的类对象。例如:
Class cl1 = Random.class;
Class cl2 = int.class;
Class cl3 = Double[].class;
一个Class对象实际上表示的是一个类型,而这个类型未必一定是一种类。例如,int不是类,但int.class是一个Class类型的对象。

还有一个很有用的方法newInstance(),可以用来动态地创建一个类的实例:
e.getClass().newInstance();
创建了一个与e具有相同类类型的实例。newInstance方法调用默认的构造器初始化新创建的对象。如果这个类没有默认的构造器,就会抛出一个异常。
将forName与newInstance配合起来使用,可以根据存储在字符串中的类名创建一个对象。
String s = "java.util.Random";
Object m = Class.forName(s).newInstance();

捕获异常
当程序运行过程中发生错误时,就会”抛出异常“。抛出异常比终止程序要灵活得多,这是因为可以提供一个”捕获“异常的处理器(handler)对异常情况进行处理。

将可能抛出已检查异常的一个或多个方法调用代码放在try块中,然后在catch子句中提供处理器代码。
try
{
  statements that might throw exceptions
}
catch (Exception e)
{
  handler action
}
下面是一个实例:
try
{
  String name = ...;
  Class cl = Class.forName(name);
  do something with cl;
}
catch (Exception e)
{
  e.printStackTrace();
}
如果类名不存在,则将跳过try块中的剩余代码,程序直接进入catch子句。如果try块中没有抛出任何异常,那么会跳过catch子句的处理器代码。

利用反射分析类的能力
在java.lang.reflect包中有三个类Field、Method和Constructor分别用于描述类的域、方法和构造器。这三个类都有一个叫做getName的方法,用来返回项目的名称。

Class类中getFields、getMethod和getConstructors方法将分别返回类提供的public域、方法和构造器数组,其中包括超类的公有成员。Class类的getDeclareFields、getDeclareMethods和getDeclareConstructors方法将分别返回类中声明的全部域、方法和构造器,其中包括私有和受保护成员,但不包括超类的成员。

查看对象域的关键方法是Field类中的get方法。如果f是一个Field类型的对象(例如,通过getDeclaredField得到的对象),obj是某个包含f域的类的对象,f.get(obj)将返回一个对象,其值为obj域的当前值。

Employee harry = new Employee("Harry Hacker",35000,10,1,1989);
Class cl = harry.getClass();//the class object representing Employee
Field f = cl.getDeclaredField("name");//the name field of the Employee class
object v = f.get(harry);

调用任意方法
对于静态方法,第一个参数可以被忽略,即可将它设置为null。

假设用m1代表Employee类的getName方法:
String n = (String) m1.invoke(harry);
如果返回类型是基本类型,invoke方法会返回其包装器类型。假设m2表示Employee类的getSalary方法,那么返回的对象实际上是一个Double,必须相应地完成类型转换。可使用自动拆箱将它转换为一个double:
double s = (Double) m2.invoke(harry);
getMethod的签名是:
Method getMethod(String name, Class...parameterTypes)
例如如何获得Employee类的getName方法和raiseSalary方法的方法指针:
Method m1 = Employee.class.getMethod("getName");
Method m2 = Employee.class.getMethod("raiseSalary",double.class);

继承设计技巧
1.将公共操作和域放在超类
2.不要使用受保护的域
3.使用继承实现“is-a”关系
4.除非所有继承的方法都有意义,否则不要使用继承
5.在覆盖方法时,不要改变预期的行为
6.使用多态,而非类型信息
7.不要过多地使用反射

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值