06_面向对象高级篇_01-继承、Object类、抽象类、final关键字
本章章节
> 6.1 继承的基本概念
> 6.2 Object类
> 6.3抽象类
> 6.4 final 关键字
> 6.5接口(interface)
> 6.6对象多态性
> 6.7内部类
> 6.8枚举
> 6.9 Java系统常见类
> 6.10 Java垃圾回收机制
.本章摘要:
(1)先创建父类的变量数组;
(2)利用数组元素创建子类的对象...
在上一章已经了解了类的基本使用方法,对于面向对象的程序而言,它的精华在于类的继承和多态,可以以既有的类为基础,进而派生出新的类。通过这种方式,便能快速地开发出新的类,而不需编写相同的程序代码,这也就是程序代码再利用的概念。
本节将介绍继承的概念以及其实际的应用。
6.1 继承的基本概念
在讲解继承的基本概念之前,可以先想一想这样一个问题:现在假设有一个Person类,里面有name与age两个属性,而另外一个Student类,需要有name、age、school三个属性,如图6-1所示,从这里可以发现Person中已经存在有name和age两个属性,所以不希望在Student类中再重新声明这两个属性,这个时候就需要考虑是不是可以将Person类中的内容继续保留到Student类中,也就是引出了接下来所要介绍的类的继承概念。
图6-1 Person与Student的继承关系
java中一个类只能继承于另一个类。我们将被继承的类称之为父类(基类),继承类称之为子类(派生类)。在java中用关键字extends来实现单继承,Java类的继承,可用下面的语法来表示:
class 父类// 定义父类 { } class 子类 extends 父类// 用extends关键字实现类的继承 { }
需要注意的是:实现继承关系的类之间有着必然的联系,不能将不相关的类实现继承。比如:人类不能继承自鸟类。
范例:TestPersonStudentDemo.java
class Person { String name; int age; } class Student extends Person { String school; } public class TestPersonStudentDemo { public static void main(String[] args) { Student s = new Student(); s.name = "张三"; // 访问Person类中的name属性 s.age = 25; // 访问Person类中的age属性 s.school = "北京大学"; // 访问Student类中的school属性 System.out.println("姓名:" + s.name + ",年龄:" + s.age + ",学校:" + s.school); } }
输出结果:
姓名:张三,年龄:25,学校:北京大学
程序说明:
1、程序1~5行声明一个名为Person的类,里面有name与age两个属性。
2、程序第6~9行声明一个名为Student的类,并继承自Person类。
3、程序第14行声明并实例化一个Student类的对象
4、程序第15~17行分别用Student类的对象调用程序中的name、age、school属性。
由上面的程序可以发现,在Student类中虽然并未定义name与age属性,但在程序外部却依然可以调用name或age,这是因为Student类直接继承自Person类,也就是说Student类直接继承了Person类中的属性,所以Student类的对象才可以访问到父类中的成员。以上所述,可用图6-2表示:
图6-2 Person与Student类的继承图
再举个例子:
假设我们开发某公司的员工管理系统,已知类Manager和类Employee,代码如下:
class Employee { public String name; //姓名 public int work_age; //工龄 public float salary = 0.0f; //薪水 public String getEmpDetails() //员工信息
{....} } class Manager { public String name; //姓名 public int work_age; //工龄 public float salary; //薪水 public String dept; //职位
public String getEmpDetails()//员工信息 {....} }
通过分析得知,在类Employee和类Manager中存在许多共同的属性和行为,在现实生活中,Manager是公司Employee之一。因此,我们可以将Manager类定义成Employee类的子类,修改类Manager如下:
class Manager extends Employee { public String dept; //职位 public String getEmpDetails() { return "This is Manager!"; } }
用UML(Unified Modeling Language,统一建模语言)中的类图表示如图6-3:
图6-3 类图
通过上面两个例子,我们可以发现继承带来的一些好处:
·减少代码冗余
从上面的例子就可以看出,类Manager通过继承而无需再定义属性(name, work_age, salary),从而减少了代码量。
·维护变得简单
假设公司要求给所有员工添加生日这一属性,那么,在没有继承时,我们的维护将变得困难(需修改每一个级别的员工类)。而现在只需要在Employee里面加入即可。
·扩展变得容易
当一个新的员工级别类需创建时,我们只需将该类继承所有员工父类Employee,接着再定义属于该员工的特有属性即可。
注意:
子类只能继承父类所有的公有成员。隐含的继承了父类中的私有属性。在使用继承的时候也应注意:子类是不能直接访问父类中的私有成员的,但可以调用父类中的非私有方法。在 java 中只允许单继承,而不允许多重继承,也就是说一个子类只能有一个父类,但是 java 中却允许多层继承和多次继承。
多重继承:
图6-4 多重继承
class A {} class B {} class C extends A, B {}
由上面可以发现类C同时继承了类A与类B,也就是说C类同时继承了两个父类,这在JAVA中是不允许的。
多层继承:
图6-5 多层继承
class A { } class B extends A { } class C extends B { }
由上面可以发现类B继承了类A,而类C又继承了类B,也就是说类B是类A的子类,而类C则是类A的孙子类。
多次继承:
图6-6 多次继承
class A {} class B extends A {} class C extends A {}
由上面可以发现类B继承了类A,而类C也继承了类A,也就是说一个类可以被多个类继承。即一个父类可以派生出多个子类,但一个子类只能由一个父类。
6.1.1 子类对象的实例化过程
既然子类可以继承父类中的方法与属性,那父类中的构造方法呢?构造方法是不能被继承!一个类得到构造构造方法只有两种途径:自定义构造方法或使用JVM分配的缺省构造方法。但是,可以在子类中访问父类的构造方法。请看下面的范例:
范例:TestPersonStudentDemo1.java
class Person { String name; int age; public Person() // 父类的构造方法 { System.out.println("1.public Person(){}"); } } class Student extends Person { String school; public Student() // 子类的构造方法 { System.out.println("2.public Student(){}"); } } public class TestPersonStudentDemo1 { public static void main(String[] args) { Student s = new Student(); } }
输出结果:
1.public Person(){}
2.public Student(){}
程序说明:
1、程序1~9行,声明一个Person类,此类中有一个无参构造方法。
2、程序10~17行,声明一个Student类,此类继承自Person类,此类中也有一个无参构造方法。
3、程序22行,声明并实例化一个Student类的对象s。
从程序输出结果中可以发现,虽然程序第22行实例化的是子类的对象,但是程序却先去调用父类中的无参构造方法,之后再调用了子类本身的构造方法。所以由此可以得出结论,子类对象在实例化时会默认先去调用父类中的无参构造方法,之后再调用本类中的相应构造方法。
实际上在本范例中,实际上在子类构造方法的第一行默认隐含了一个“super()”语句,上面的程序如果改写成下面的形式,也是可以的:
class Student extends Person { String school; public Student() // 子类的构造方法 { super(); //ß实际上在程序的这里隐含了这样一条语句 System.out.println("2.public Student(){}"); } }
小提示:
在子类继承父类的时候经常会有下面的问题发生,请看下面的范例:
范例:TestPersonStudentDemo2.java
class Person { String name; int age; public Person(String name, int age) // 父类的构造方法 { this.name = name; this.age = age; } } class Student extends Person { String school; public Student() // 子类的构造方法 { } } public class TestPersonStudentDemo2 { public static void main(String[] args) { Student s = new Student(); } }
编译结果:
TestPersonStudentDemo2.java:15: Person(java.lang.String,int) in Person cannot be applied to()
{}
^
1 error
由编译结果可以发现,系统提供的出错信息是因为无法找到Person类,所以造成了编译错误,这是为什么呢?在类 Person 中提供了一个有两个参数的构造方法,而并没有明确的写出无参构造方法,在前面已提到过,如果程序中指定了构造方法,则默认构造方法不会再生成,本例就是这个道理。由于第22行实例化子类对象时找不到父类中无参构造方法,所以程序出现了错误,而只要在Person类中增加一个什么都不做的构造方法,就这一问题就可以解决了。对范例 TestPersonStudentDemo2.java作相应的修改就形成了范例 TestPersonStudentDemo3.java,如下所示:
范例:TestPersonStudentDemo3.java
class Person { String name; int age; public Person() { }// 增加一个什么都不做的无参构造方法 public Person(String name, int age) // 父类的构造方法 { this.name = name; this.age = age; } } class Student extends Person { String school; public Student()// 子类的构造方法 { } } public class TestPersonStudentDemo3 { public static void main(String[] args) { Student s = new Student(); } }
在程序的第6行声明了一个Person类的无参的且什么都不做的构造方法,所以程序在编译就可以正常通过了。
6.1.2 super 关键字的使用
在上面的程序中曾经提到过super的使用,那super 到底是什么呢?从TestPersonStudentDemo1中应该可以发现,super关键字出现在子类中,而且是去调用了父类中的构造方法,所以可以得出结论:super主要的功能是调用父类的构造函数、访问父类中的属性或方法。
既然知道了super的功能,那么对于TestPersonStudentDemo2.java中的错误,我们还可以用如下的方法来进行解决:
范例:TestPersonStudentDemo4.java
class Person { String name; int age; public Person(String name, int age) // 父类的构造方法 { this.name = name; this.age = age; } } class Student extends Person { String school; public Student() // 子类的构造方法 { super("张三", 25);// 在这里用super显示调用父类中带两个参数的构造方法 } } public class TestPersonStudentDemo4 { public static void main(String[] args) { Student s = new Student(); s.school = "北京"; // 为Student类中的school赋值 System.out.println("姓名:" + s.name + ",年龄:" + s.age + ",学校:" + s.school); } }
输出结果:
姓名:张三,年龄:25,学校:北京
程序说明:
1、程序第1~10行声明一名为Person的类,里面有name与age两个属性,并声明了一个含有两个参数的构造方法。
2、程序第11~18行,声明一个名为Student的类,此类继承自Person类,在16~20行声明子类的构造方法,在此方法中用super("张三",25)调用父类中有两个参数的构造方法。
3、程序在第23行声明并实例化一个Student类的对象s。第24行为Student对象s中的school赋值为“北京”。
本例与范例TestPersonStudentDemo3.java的程序基本上是一样的,唯一的不同是在子类的构造方法中明确的指明调用的是父类中有两个参数的构造方法,所以程序在编译时不再去找父类中无参的构造方法。
注意:
用super调用父类中的构造方法,只能放在程序的第一行。
super关键字不仅可以调用父类中的构造方法,也可以调用父类中的属性或方法,如下:
super.父类中的属性;
super.父类中的方法();
范例:TestPersonStudentDemo5.java
class Person { String name; int age; public Person() // 父类的构造方法 { } public String talk() { return "我是:" + this.name + ",今年:" + this.age + "岁"; } } class Student extends Person { String school; public Student(String name, int age, String school) // 子类的构造方法 { // 在这里用super调用父类中的属性 super.name = name; super.age = age; // 调用父类中的talk()方法 System.out.print(super.talk()); // 调用本类中的school属性 this.school = school; } } public class TestPersonStudentDemo5 { public static void main(String[] args) { Student s = new Student("张三", 25, "北京"); System.out.println(",学校:" + s.school); } }
输出结果:
姓名:张三,年龄:25,学校:北京
程序说明:
1、程序第1~12行声明一个名为Person的类,并声明name与age两个属性、一个返回String类型的talk()方法,以及一个无参构造方法。
2、程序第13~26行声明一个名为Student的类,此类直接继承自Person类。
3、程序第19、20行,通过super.属性的方式调用父类中的name与age属性,并分别赋值。
4、程序第22行调用父类中的talk()方法并打印信息。
从上面的程序中可以发现,子类Student可以通过super调用父类中的属性或方法,当然在本例中,如果程序第19、20、22行换成this调用也是可以的。
关于this与super关键字的使用,对于一些初学者来说可能有些混淆,表6-1对this与super的差别进行比较,如下所示:
表6-1 this与super的比较
从上表中不难发现,用super或this调用构造方法时都需要放在首行,所以,super与this调用构造方法的操作是不能同时出现的。super与this不能出现在static修饰的方法中。如果子类构造方法中没有使用super来显示调用父类的构造方法的话,系统将会在执行子类构造方法之前,隐式调用父类无参构造方法。
6.1.3 限制子类的访问
前面已经讲过,在使用继承的时候应注意:子类只能继承父类所有的公有成员,隐含的继承了父类中的私有属性。子类是不能直接访问父类中的私有成员的,但可以调用父类中的非私有方法。在继承中,有些时候,父类并不希望子类可以访问自己的类中的全部的属性或方法,所以需要将一些属性与方法隐藏起来,不让子类去使用,所以在声明属性或方法时往往加上“private”关键字,表示私有。为了保证父类良好的封装性,不会被子类随意改变,设计父类通常应该遵循如下规则:
·尽量隐藏父类的内部数据,把父类的所有属性设置为private访问类型,不用让子类直接访问父类属性。
·父类中的一些不能被子类复写的方法,应该使用private将其隐藏起来,让子类无法访问该方法。如果父类的方法需要外部类调用,但不希望子类覆盖,可以使用 public final联合修饰,如果希望父类某个方法被子类重写,但不希望其他类自由访问,可以使用protected修饰。
范例:TestPersonStudentDemo6.java
class Person { // 在这里将属性封装 private String name; private int age; } class Student extends Person { // 在这里访问父类中被封装的属性 public void setVar() { name = "张三"; age = 25; } } class TestPersonStudentDemo6 { public static void main(String[] args) { new Student().setVar(); } }
编译结果:
TestPersonStudentDemo6.java:12: namehas private access in Person
name = "张三" ;
^
TestPersonStudentDemo6.java:13: age has private access in Person
age = 25 ;
^
2 errors
由编译器的错误提示可以发现,name与age属性在子类中无法进行访问。
小提示:
上面所示范例,只要父类中的属性被“private”声明的话,那么子类就无法直接访问到它了。那么有没有什么其它方法可以访问到这些私有属性呢?
// 在Person类中加上:
// 添加了两个setXxx()方法 public void setName(String name) { this.name=name; } public void setAge(int age) { this.age=age; } // 在Student类中加上: // 在这里通过父类的公共接口去访问父类中被封装的属性 public void setVar() { super.setName("张三"); super.setAge(25); }
6.1.4 访问修饰符
在java中是通过各种访问区分符来实现数据封装的,共分为四种访问级别(访问控制级别从小到大):private(私有)、friendly(缺省default)、protected(受保护)、public(公共)。
注意:以上四种访问修饰符可以作用于任何变量、属性和方法,类只可以定义为缺省或公共级别(嵌套类除外)。四种修饰符的访问权限如表6-2所示:
表6-2 四种修饰符
访问控制符的使用原则
类里的绝大部分属性都应该使用private修饰,除了一些static修饰的、类似全局变量的属性,才可以考虑使用public修饰。此外有些方法只是用来辅助实现该类的其他方法,这些方法被称为工具方法,工具方法也应该使用private修饰。
如果某个类主要用做其他类的父类,该类里包含的大部分方法可能仅希望被其子类重写,而不想被外界直接调用,则应该使用protected修饰这些方法。
希望暴露出来给其他类自由调用的方法应该使用public修饰。类的构造器通过使用public修饰,暴露给其他类中创建该类的对象,因为顶级类通常都希望被其他类自由使用,所以大部分顶级类都使用public修饰。
6.1.5 复写
“复写”也叫重写、覆盖,与“重载”相似,它们均是Java“多态”的技术之一,所谓“重载”,即是方法名称相同,但却可在不同的场合做不同的事。当一个子类继承一父类,而子类中的方法与父类中的方法的名称,参数个数、类型都完全一致时,就称子类中的这个方法复写了父类中的方法。同理,如果子类中重复定义了父类中已有的属性,则称此子类中的属性复写了父类中的属性。
方法复写的基本格式如下:
class Super { 访问权限 方法返回值类型 方法1(参数1) {} } class Sub extends Super { 访问权限 方法返回值类型 方法1(参数1)//à 复写父类中的方法 {} }
方法的重写要遵循“两同两小一大”的规则:
a. 重写(覆盖):子类和父类方法名相同,参数列表相同,这是“两同”。
b. 作用域:指的子类方法的访问权限应该比父类方法更大或者相等,这是“一大”。
c. 返回类型和抛出异常:“两小”指的是子类方法返回值类型应该比父类方法返回值类型更小或者相等,子类方法声明抛出的异常应比父类方法声明抛出异常类更小或者相等。
另外还要注意的是:覆盖方法和被覆盖方法要么都是类方法,要么都是对象方法,不能一个是类方法,一个是对象方法。
对于一个private方法,因为它仅在当前类中可见,其他子类无法访问该方法,所以子类无法重写该方法,如果在子类中定义一个与父类private方法有相同方法名、相同形参列表、相同返回值类型的方法,也不是方法重写,只是重新定义了一个新方法。
重载与复写的比较如表6-3所示
表6-3 重载与重写的比较
|
重载 |
覆盖(重写) |
单词 |
Overload |
Override |
概念 |
方法名称相同,参数的类型或个数或顺序不同 |
方法名称相同,参数类型及个数相同,方法的访问权限不能更加严格 |
范围 |
发生在同一个类中 |
发生在不同类中,有继承关系 |
范例:TestOverDemo1.java
class Person { String name; int age; public String talk() { return "我是:" + this.name + ",今年:" + this.age + "岁"; } } class Student extends Person { String school; public Student(String name, int age, String school) { // 分别为属性赋值 this.name = name; this.age = age; this.school = school; }
// 此处复写Person中的talk()方法 public String talk() { return "我在" + this.school + "上学"; } } class TestOverDemo1 { public static void main(String[] args) { Student s = new Student("张三", 25, "北京"); // 此时调用的是子类中的talk()方法 System.out.println(s.talk()); } }
输出结果:
我在北京上学
程序说明:
1、程序1~9行声明一个名为Person的类,里面声明了name与age两个属性,和一个talk()方法。
2、程序10~25行声明一个名为Student的类,此类继承自Person类,也就继承了name与age属性,同时声明了一个与父类中同名的talk()方法,也可以说此时Student类中的talk()方法复写了Person类中的talk()方法。
3、程序第30行实例化一个子类对象,并同时调用子类构造方法为属性赋初值。
4、程序第32行用子类对象调用talk()方法,但此时调用的是子类中的talk()方法。
由输出结果可以发现,在子类中复写了父类中的talk()方法,所以子类对象在调用talk()方法时,实际上调用的是子类中被复写好了的方法。另外可以发现,子类的talk()方法与父类的talk()方法,在声明权限时,都声明为public,也就是说这两个方法的访问权限都是一样的。
注意:
子类复写父类中的方法时,被子类复写的方法不能拥有比父类更严格的访问权限,即:
class Person { public String talk() { } } class Student extends Person { // 此处会因为权限出现错误 String talk() { } }
在子类中talk()方法处并没有声明权限,如果不声明则权限为default,但父类中的talk方法有public,而public权限要高于default权限,所以此时子类的方法比父类中拥有更严格的访问权限,所以会出现错误。
由TestOverDemo1.java程序中可以发现,在程序32行调用talk()方法实际上调用的只是子类的方法,那如果现在需要调用父类中的方法该如何实现呢?请看下面的范例,下面的范例,修改自TestOverDemo1.java。
范例:TestOverDemo2.java
class Person { public String talk() { } } class Student extends Person { // 此处会因为权限出现错误 String talk() { } } class Person { String name; int age; public String talk() { return "我是:" + this.name + ",今年:" + this.age + "岁"; } } class Student extends Person { String school;
public Student(String name, int age, String school) { // 分别为属性赋值 this.name = name; this.age = age; this.school = school; } // 此处复写Person类中的talk()方法 public String talk() { return super.talk() + ",我在" + this.school + "上学"; } } class TestOverDemo2 { public static void main(String[] args) { Student s = new Student("张三", 25, "北京"); //此时调用的是子类中的talk()方法 System.out.println(s.talk()); } }
输出结果:
我是:张三,今年:25岁,我在北京上学
程序说明:
1、程序1~9行声明Person类,里面声明了name与age两个属性,和一个talk()方法。
2、程序10~25行声明Student类,此类继承自Person,也就继承了name与age属性,同时声明了一个与父类中同名的talk()方法,也可以说此时Student类中的talk()方法复写了Person类中的talk()方法,但在23行通过super.talk()方式,调用父类中的talk()方法。
3、程序第30行实例化一个子类对象,并同时调用子类构造方法为属性赋初值。
4、程序第32行用子类对象调用talk()方法,但此时调用的是子类中的talk()方法。
由上面程序可以发现,在子类可以通过“super.方法()”的方式调用父类中被子类复写的方法。
6.2 Object 类
Java中有一个比较特殊的类,就是Object类,它是所有类的父类,该类位于java.lang包下面。如果一个类没有使用extends关键字明确标识继承另外一个类,那么这个类就默认继承Object类。因此,Object类是Java类层中的最高层类,是所有类的超类。换句话说,Java中任何一个类都是它的子类。由于所有的类都是由Object类衍生出来的,所以Oject类中的方法适用于所有类。
public class Person // 当没有指定父类时,会默认Object类为其父类 { … }
上面的程序等价于:
public class Person extends Object { … }
Object类中有很多方法,除了与线程相关的方法(notify、notifyAll、wait以后再讲)之外。还有很多其他的方法,如:clone、equals、finalize、getClass、hashCode、toString。
·clone
clone方法赋予对象以克隆的能力。子类通过覆盖clone方法并实现Cloneable接口来实现克隆能力,否则调用clone方法抛出异常。
函数原型:protected Object clone() throws CloneNotSupportedException;
函数说明:创建并返回一个对象的副本。对原对象的修改不会影响到副本;对副本的修改也不会影响到原对象的值。注意跟两个对象直接赋值的区别。
范例:TestClone.java
class User implements Cloneable { private int id; private String name; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); // 标准写法,总是返回super.clone() } } public class TestClone { public static void main(String[] args) { try { User user = new User(); User u = (User) user.clone(); // 注意跟User u = user;的区别 user.setId(100); user.setName("张三"); System.out.println(u.getId()); System.out.println(u.getName()); } catch (CloneNotSupportedException e) { e.printStackTrace(); } } }
·getClass
getClass()获得当前对象的类型。Java中有Class类,用以描述类型信息。如用下面的语句:Class theClass = "hello".getClass();得到的就是字符串的类型,Class类是Java反射技术中重要的类,我们在后面会讲解。
函数原型:public final Class<?> getClass();
范例:TestGetClass.java
class AAA { } public class Test { public static void main(String[] args) { //基本数据类型 System.out.println(int.class); System.out.println(double.class); //引用数据类型 System.out.println("hello".getClass()); System.out.println(new AAA().getClass()); } }
一个设计比较好的类,需要覆盖Object类中的三个方法:
public String toString()
public boolean equals(Object obj)
public int hashCode()
关于这三个函数的使用可以查阅一下JDK的帮助文档:
·toString
toString是Java确保所有“对象”都可以以字符串表达的方法,当需要将对象转换为字符串时该方法被自动调用。子类可以覆盖该方法以形成自己特别的字符串表达形式。
函数原型:public String toString();
函数说明:返回该对象的字符串表示。通常,toString方法会返回一个“以文本方式表示”此对象的字符串。结果应是一个简明但易于读懂的信息表达式。建议所有子类都重写此方法。
函数返回:默认情况下Object类的toString方法返回一个字符串,该字符串由类名、@标记符和此对象哈希码的无符号十六进制表示组成。换句话说,该方法返回一个字符串,它的值等于:getClass().getName() + '@' + Integer.toHexString(hashCode())
下面来看看toString()的使用,此方法是在打印对象时被调用的,下面有两个范例,一个是没复写了toString()方法,另一个是复写了toString()方法,可比较两者的区别。
范例:TestToStringDemo1.java
class Person extends Object { String name = "张三"; int age = 25; } class TestToStringDemo1 { public static void main(String[] args) { Person p = new Person(); System.out.println(p); } }
输出结果:
Person@c17164
程序说明:
1、程序1~5行声明一个Person类,并明确指出继承自Object类。
2、程序第10行声明并实例化一个Person类的对象p,11行打印对象。
从上面的程序中可以发现,在打印对象p的时候实际上打印出的是一些无序的字符串,下面的范例复写了Object类中的toString()方法。
范例:TestToStringDemo2.java
class Person extends Object { String name = "张三"; int age = 25; // 复写Object类中的toString()方法 public String toString() { return "我是:" + this.name + ",今年:" + this.age + "岁"; } } class TestToStringDemo2 { public static void main(String[] args) { Person p = new Person(); System.out.println(p); } }
输出结果:
我是:张三,今年:25岁
与TestToStringDemo1.java程序相比,程序TestToStringDemo2.java程序在Person类中明确复写了toString()方法,这样在打印对象p的时候,实际上是去调用了toString()方法,只是并没有明显的指明调用toString()方法而已,此时第16行相当于:
System.out.println(p.toString());
·equals
java程序中测试两个变量是否相等有两种方式,一种是利用==运算符,另外一种是利用equals方法。Object默认提供的equals方法只是比较对象的地址,即Object类的equals方法比较的结果与==运算符比较的结果完全相同。子类可覆盖该方法来比较对象内容是否相等。
函数原型:public boolean equals(Object obj);
函数说明:指示其他某个对象是否与此对象“相等”。
equals方法在非空对象引用上实现相等关系:
·自反性:对于任何非空引用值x,x.equals(x)都应返回true。
·对称性:对于任何非空引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)才应返回true。
·传递性:对于任何非空引用值x、y和z,如果x.equals(y)返回true,并且y.equals(z)返回true,那么x.equals(z)应返回true。
·一致性:对于任何非空引用值x和y,多次调用x.equals(y)始终返回true或始终返回 false,前提是对象上equals比较中所用的信息没有被修改。
·对于任何非空引用值x,x.equals(null) 都应返回false。
Object类的equals方法实现对象上差别可能性最大的相等关系;即对于任何非空引用值x和y,当且仅当x和y引用同一个对象时,此方法才返回true(x == y具有值true)。
注意:当此方法被重写时,通常有必要重写hashCode方法,以维护hashCode方法的常规协定,该协定声明相等对象必须具有相等的哈希码。
函数参数:obj - 要与之比较的引用对象。
函数返回:如果此对象与obj参数相同,则返回true;否则返回false。
请看下面的范例,下面的范例是一个没有复写equals()方法的范例。
范例:TestOverEquals.java
class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } } class TestOverEquals1 { public static void main(String[] args) { Person p1 = new Person("张三", 25); Person p2 = new Person("张三", 25); // 判断p1和p2的内容是否相等 System.out.println(p1.equals(p2) ? "是同一个人!" : "不是同一个人"); } }
输出结果:
不是同一个人
程序说明:
1、程序第1~10行声明一个Person类,并声明一个构造方法为类的属性初始化。
2、程序第15、16行声明两个Person对象p1、p2,其内容相等。
3、程序第18行是比较两对象的内容是否相等。
从上面的程序中可以发现,两对象的内容完全相等,但为什么比较的结果是不相等呢?因为p1与p2的内容分别在不同的内存空间,指向了不同的内存地址,所以在用equals比较时,实际上是调用了Object类中的equals方法,但可以发现此方法并不好用,所以在开发中往往需要复写equals方法,请看下面的范例:
范例:TestOverEquals2.java
class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } // 复写父类(Object类)中的equals方法 public boolean equals(Object obj) { if (this == obj) return true; //是否是自己与自己判断 if (obj == null) return false; //不与null进行比较 if (this.getClass() != obj.getClass()) return false;//检测两个对象是否属于同一类 if (obj instanceof Person) // 判断Object类对象是否是Person的实例 { Person p2 = (Person) obj;// 如果是Person类实例,则进行向下转型 // 调用String类中的equals方法 return (this.name.equals(p2.name) && this.age == p2.age); } return false; // 如果不是Person类实例,则直接返回false } } public class TestOverEquals2 { public static void main(String[] args) { Person p1 = new Person("张三", 25); Person p2 = new Person("张三", 25); //判断p1和p2的内容是否相等,如果相等,则表示是一个人,反之,则不是 System.out.println(p1.equals(p2) ? "是同一个人!" : "不是同一个人"); } }
输出结果:
是同一个人!
程序说明:
1、程序1~21行声明一个Person类,并在类中复写了Object类的equals方法。
2、第13行,判断传进去的实例对象obj是否属于Person类的实例化对象,如果是,则进行转型,否则返回false。
3、程序第17行分别比较两个对象的内容是否相等,如果不相等,则返回false 。
5、第29行,通过p1调用equals方法,并将p2对象的实例传到equals方法之中,比较两对象是否相等。
·hashCode
函数原型:public int hashCode();
函数说明:返回该对象的哈希码值。
hashCode 的常规协定是:
·在Java应用程序执行期间,在对同一对象多次调用hashCode方法时,必须一致地返回相同的整数,前提是将对象进行equals比较时所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。
·如果根据equals(Object)方法,两个对象是相等的,那么对这两个对象中的每个对象调用hashCode方法都必须生成相同的整数结果。
·两个对象利用equals(java.lang.Obj ect)方法比较不相等,但两个对象的hashCode可以相等(程序员应该意识到,为不相等的对象生成不同整数结果可以提高哈希表的性能。)
实际上,由 Object 类定义的hashCode方法确实会针对不同的对象返回不同的整数(这一般是通过将该对象的内部地址转换成一个整数来实现的)。
函数返回:此对象的一个哈希码值。
例如在TestOverEquals2.java的Person类中增加如下代码:
public int hashCode(){ return name.hashCode() + age; }
6.3 抽象类
前面对类的继承进行了初步的讲解。通过继承,可以从原有的类派生出新的类。原有的类称为基类或父类,而新的类则称为派生类或子类。通过这种机制,派生出的新的类不仅可以保留原有的类的功能,而且还可以拥有更多的功能。
除了上述的机制之外,Java也可以创建一种类专门用来当作父类,这种类称为“抽象类”。
抽象类的概念:包含一个抽象方法的类就称为抽象类。
抽象方法:只声明而未实现(未实现,就是说方法声明后面没有“{}”)的方法称为抽象方法。抽象方法必须使用abstract关键字声明。
抽象类的作用有点类似“模版”,其目的是要设计者依据它的格式来修改并创建新的类。但是并不能直接由抽象类创建对象,只能通过抽象类派生出新的类,再由它来创建对象。例如:Animal 类,动物的叫声方法enjoy(),父类定义和实现其叫声,没有什么意义,因为每个子类,具体的某种动物,它的叫声是不一样的。因此,在父类Animal中,只需要声明一下,有叫声这个方法,具体的实现,就交由子类来进行。
抽象类定义规则:
·抽象类和抽象方法都必须用abstract关键字来修饰。
·抽象类不能被实例化,也就是不能用new关键字去产生对象。但是可以通过非抽象子类来创建抽象类的对象。
·抽象方法只需声明,而不需实现。
·含有抽象方法的类必须被声明为抽象类,抽象类的子类必须复写所有的抽象方法后才能被实例化,否则这个子类还是个抽象类。
抽象类的定义格式:
注意:
在抽象类定义的语法中,方法的定义可分为两种:一种是一般的方法,它和先前介绍过的方法没有什么两样;另一种是“抽象方法”,它是以abstract关键字为开头的方法,此方法只声明了返回值的数据类型、方法名称与所需的参数,但没有定义方法体。
范例:TestAbstractDemo1.java
abstract class Person { String name; int age; String occupation; //职业 public abstract String talk(); // 声明一个抽象方法talk() } class Student extends Person // Student类继承自Person类 { public Student(String name, int age, String occupation) { this.name = name; this.age = age; this.occupation = occupation; } public String talk() // 复写父类的talk()抽象方法 { return "学生——>姓名:" + this.name + ",年龄:" + this.age + ",职业:" + this.occupation + "!"; } } class Worker extends Person // Worker类继承自Person类 { public Worker(String name, int age, String occupation) { this.name = name; this.age = age; this.occupation = occupation; } public String talk() // 复写父类的talk()抽象方法 { return "工人——>姓名:" + this.name + ",年龄:" + this.age + ",职业:" + this.occupation + "!"; } } class TestAbstractDemo1 { public static void main(String[] args) { Student s = new Student("张三", 20, "学生"); Worker w = new Worker("李四", 30, "工人"); System.out.println(s.talk()); System.out.println(w.talk()); } }
输出结果:
学生——>姓名:张三,年龄:20,职业:学生!
工人——>姓名:李四,年龄:30,职业:工人!
程序说明:
1、程序1~7行声明一个名为Person的抽象类,在Person中声明了三个属性,和一个抽象方法——talk()。
2、程序8~20行声明一个Student类,此类继承自Person类,而且此类不为抽象类,所以需要复写Person类中的抽象方法——talk()。
3、程序21~33行声明一个Worker类,此类继承自Person类,而且此类不为抽象类,所以需要复写Person类中的抽象方法——talk()。
4、程序第38、39行分别实例化Student类与Worker类的对象,并调用各自构造方法初始化类属性。
5、程序40、41行分别调用各自类中被复写的talk()方法。
可以发现两个子类Student、Worker都分别按各自的要求复写了talk()方法。上面的程序可由图6-7表示,如下所示:
图6-7 抽象类的继承关系
小提示:
与一般类相同,在抽象类中,也可以拥有构造方法,此时也可以在子类的构造方法中通过super来显示调用父类的构造函数。
abstract class Person { String name; int age; String occupation; public Person(String name, int age, String occupation) { this.name = name; this.age = age; this.occupation = occupation; } public abstractString talk(); } class Student extends Person { public Student(String name, int age, String occupation) { // 父类没有无参构造方法,所占在这里要明确调用抽象类中的构造方法 super(name, age, occupation); } public String talk() { return "学生——>姓名:" + this.name + ",年龄:" + this.age + ",职业:" + this.occupation + "!"; } } class TestAbstractDemo2 { public static void main(String[] args) { Student s = new Student("张三", 20, "学生"); System.out.println(s.talk()); } }
输出结果:
学生——>姓名:张三,年龄:20,职业:学生!
从上面的程序中可以发现,抽象类也可以像普通类一样,有构造方法、一般方法、属性,更重要的是还可以有一些抽象方法,留给子类去实现,而且在抽象类中声明构造方法后,在子类中必须明确调用。
抽象类的作用:
抽象类是从多个具体类中抽象出来的父类,它具有更高层次的抽象。以这个抽象类作为其子类的模板,从而避免了子类设计的随意性。
抽象类体现的就是一种模板模式的设计,抽象类作为多个子类的通用模板,子类在抽象类的基础上进行扩展、改造,但子类总体上会大致保留抽象类的行为方式。
6.4 final 关键字
在Java中声明类、属性和方法时,可使用关键字final来修饰。
1、final标记的类不能被继承。
2、final标记的方法不能被子类复写。
3、final标记的变量(成员变量或局部变量)即为常量,只能赋值一次。
final修饰的类不可有子类。如java.lang.String、java.lang.Math都是一个final类,它们不可以有子类。
范例:TestFinalDemo1.java
final class Person { } class Student extends Person { }
编译结果:
TestFinalDemo1.java:4: cannot inherit from final Person
class Student extends Person
^
1 error
final修饰方法不可被重写,java提供的Object类中有一个final方法:getClass(),不希望任何子类重写这个方法。final修饰的方法不能重写,但可以重载。
范例:TestFinalDemo2.java
class Person { // 此方法声明为final不能被子类复写 final public String talk() { return "Person:talk()"; } } class Student extends Person { public String talk() { return "Student:talk()"; } }
编译结果:
TestFinalDemo2.java:11: talk() in Student cannot override talk() in Person; overridden method is final
public String talk()
^
1 error
Final修饰变量,称为常量。常量通常全部大写。
Final修饰成员变量和修饰局部变量的区别:
使用final修饰成员变量时,要么在定义程序变量的时候指定初始化值,要么在初始化块、构造方法为成员变量赋值。如果定义该成员变量时候指定了默认值,那么不能在初始化块、构造方法重新赋值。Final成员变量必须由程序员显示初始化,系统不会对final成员变量进行隐式初始化。
Final修饰局部变量,必须由程序员显示初始化,可以先定义,后赋值,但只能一次,不能重复赋值。
范例:TestFinalDemo3.java
class TestFinalDemo3 { public static void main(String[] args) { final int i = 10; // 修改用final修饰的变量i i++; } }
编译结果:
TestFinalDemo1.java:6: cannot assign a value to final variable i
i++;
^
1 error
可以将static和final结合起来使用,此时声明的变量称为全局常量,例如:
public static final String INFO = "aaa";
感谢阅读。如果感觉此章对您有帮助,却又不想白瞟