目录
3.1.3 方法重写(override)和 方法重载(overload)的区别
一. 封装
封装(Encapsulation)是面向对象的重要特征之一,它是由类来体现的,就是把对象的属性和方法结合成一个独立的整体,把过程和数据包围起来,并尽可能地将对象的内部实现细节进行隐藏,通过已经定义的接口来控制用户对类的修改和访问数据的程度,防止该类中的代码和数据被外部类所定义的代码随机访问。在Java中通过关键字 private 、protected 和 public 来实现封装,适当的封装可以让程序代码更容易理解和维护,也加强了代码的安全性。
对于封装而言,一个对象它所封装的是自己的属性和方法,我们只需要修改这个封装起来的对象中的实现代码,而不必修改哪些调用此对象的程序片段。良好的封装能够减少耦合、类内部的结构可以自由修改、可以对成员进行更精确的控制、隐藏相关信息,屏蔽掉实现的细节。
1.1 封装的作用:
1. 对象的数据封装特性彻底消除了传统结构方法中数据与操作分离所带来的种种问题,提高了程序的可复用性和可维护性,降低了程序员保持数据与操作内容的负担。
2. 对象的数据封装特性还可以把对象的私有数据和公共数据分离开,保护了私有数据,减少了可能的模块间干扰,达到降低程序复杂性、提高可控性的目的。
1.2 实现Java 封装的步骤
1.) 修改属性的可见性,限制对属性的访问(一般设置为 private),如:
public class Person {
private String name;
private int age;
}
这段代码中,将 name 和 age 属性设置为私有的,只有本类才能够访问,其它类都访问不了,这样就对信息进行了隐藏。
2.) 对每个值属性提供对外的公共方法访问,也就是创建一对赋值、取值方法,用于对私有属性的访问,如:
注意:如果是Boolean 类型的属性,对应的是is**** set***()方法。
public class Person{
private String name; //对属性的封装。私有的属性,只有本类自己才能够访问
private int age;
public int getAge(){ //对方法的封装。公开的方法,其它类通过new 出来的对象进行.getAge()来访问,获取到age 的值
return age;
}
public String getName(){
return name;
}
public void setAge(int age){//公开的方法,其它类通过new 出来的对象进行.setAge()来访问,赋予age 一个值
this.age = age;
}
public void setName(String name){
this.name = name; //采用 this 关键字是为了解决实例变量(private String name)和
} //局部变量(setName(String name)中的name变量)之间发生的同名的冲突。
}
以上实例中 public 方法是外部类 访问该类的成员变量的入口。通常情况下这些方法被称为 get() 、set()方法,任何想要访问类中私有成员变量的类都要通过这些 get() 、set()方法来实现。 如:
public class RunPerson{
public static void main(String args[]){
Person person = new Person(); //调用构造方法,创建一个新的实例对象
person.setName("James"); //给新创建的这个对象的name 属性 赋值 为 James
person.setAge(20); //给新创建的这个对象的age 属性 赋值 为 20
System.out.print("Name : " + person.getName()+ " Age : "+ person.getAge()); //获取这个对象的属性值,并以字符串的形式输出
}
}
1.3 包(package)
为了更好地组织类,Java提供了包机制,用于区别类名的命名空间。Java使用包机制是为了防止命名冲突,控制访问权限,提供搜索和定位类(class)、接口(Interface)、枚举(enumerations)和注释(annotation)等等。
包的作用:
1、把功能相似或相关的类或接口组织在同一个包中,方便类的查找和使用。
2、和文件夹一样,包也采用了树形目录的存储方式。同一个包中的类名字是不同的,不同的包中的类的名字是可以相同的,当同时调用两个不同包中相同类名的类时,应该加上包名加以区别。因此,包可以避免类的名字冲突。
3、包也限定了访问权限,拥有包访问权限的类才能访问某个包中的类。
不同访问修饰符的作用范围:
访问修饰符 | 访问权限 | 继承 |
private | 本类 | 不能继承 |
default | 本类 + 同包 | 同包子类可以继承 |
protected | 本类 + 同包 + 不同包的子孙类 | 可以继承 |
public | 公开 | 可以继承 |
private: 只为类本身提供。是一种封装的体现。
default: 它是针对本包设计的,它所修饰的在本包下的其他类都访问。
protected: 与public不同的是不同包下的类是不能使用的,但是其子孙类除外。所以我认为这是特意为子类设计的。
public: 具有最大访问权限。 可以被同一项目下的任何类所调用,一般用于对外的情况。
1.3.1 包的创建
包的声明应该在源文件的第一行,每个源文件只能有一个包的声明。创建包的时候需要为这个包取一个合适的名字,为了尽量使包名保持唯一性,包名通常采用小写、按倒写互联网地址的形式来定义,如:com.hank.www ,表示包文件放置的文件路径为com/hank/www。之后,如果其它的一个源文件中包含了这个包所提供的类、接口、枚举或者注释类型的时候,都必须将这个包的声明放在这个源文件的开头。如果一个源文件中没有使用包的声明,那么其中的类、函数、枚举、注释等等都将被放在一个无名的包(unnamed package)中。
1.3.2 import 关键字
为了能够使用一个包的成员,我们需要在Java程序中明确导入该包,使用“ import ”语句即可完成此功能。
在Java 源文件中 import 语句应该位于package 语句之后,且在所有类的定义之前。其中包的数量可以没有,也可以为多个。 其语法格式为:
import package1[.package2…].(classname|*);
如果在一个包中,一个类想要使用本包中的另一个类,那么该包名可以省略。
如果在包范围之外的一个类,想要访问此包中的类,比如:给出的一个A类,不在payroll这个包中,A类必须使用以下方法之一来引用其它包中的类。
使用类全名描述
payroll.Employee
用 import 关键字引入,使用通配符 "*"
import payroll.*;
使用 import 关键字引入 Employee 类:
import payroll.Employee;
二. 继承
Java继承是面向对象的显著特征之一,继承是从已有的类中派生出新的类,新的类能吸收已有类的数据,包括属性和行为等,并具备能够派生出新的子类的能力。也就是子类继承父类的属性和行为,使得子类具有父类的属性和方法,或子类从父类继承,使得子类具有和父类相同的行为,只是子类除了从父类继承来的相关性质之外还有自己特有的东西。
所以,继承所符合的关系是: is-a ,父类更一般化、通用化,子类更为特殊化、具体化。
2.1 类继承的格式
class 父类 {
}
class 子类 extends 父类{
}
案例:
public class Animal { //父类
private String name; //父类的属性
private int id;
public Animal(String myName, int myid) { //父类有参构造方法
name = myName;
id = myid;
}
public void eat(){ //父类的方法
System.out.println(name+"正在吃");
}
public void sleep(){
System.out.println(name+"正在睡");
}
public void introduction() {
System.out.println("大家好!我是" + id + "号" + name + "."); //以字符串的形式输出结果
}
}
public class Penguin extends Animal { //子类1,企鹅类
public Penguin(String myName, int myid) {
super(myName, myid); //子类1继承父类的构造方法
}
}
public class Mouse extends Animal { //子类2,老鼠类
public Mouse(String myName, int myid) {
super(myName, myid); //子类2继承父类的构造方法
}
}
这个Animal类就是作为一个父类,然后企鹅类和老鼠类继承这个类之后,就具有父类当中的属性和方法,子类就不会存在重复的代码,维护性也提高,代码也更加简洁,提高代码的复用性(复用性主要是可以多次使用,不用再多次写同样的代码)。
2.2 继承的特征
1. 子类拥有父类非 private 的属性、方法
2. 子类可以除了继承了父类中的相关属性、方法之外,还可以拥有自己的属性和方法,即子类可以对父类进行扩张
3. 子类可以用自己的方法来实现父类中的方法,即方法的重写(override)
4. Java的继承是单继承(可以通过接口来弥补这方面的不足),但是可以多重继承。单继承就是一个子类只能继承一个父类,多重继承就是 A类继承B类,B类继承C类,所以按照这个关系就是C类为B类的父类,B类为A类的父类,这就是Java继承区别于C++继承的一个特征。
2.3 关键字
可以使用 extends 和 implements 这两个关键字来实现继承,而且所有的类都是继承于 java.lang.Object,当一个类没有继承的这两个关键字,则默认继承object(这个类在 java.lang 包中,所以不需要 import)祖先类。
2.3.1 extends 关键字
在 Java 中,类的继承是单一继承,也就是说,一个子类只能拥有一个父类,所以 extends 只能继承一个类。
public class Animal{
private String name;
private int id;
public Animal(String name, int id) {
//初始化属性值
this.name = name;
this.id = id;
}
public void eat(){
//吃东西方法的具体实现
}
public void sleep(){
//睡觉方法的具体实现
}
}
//Dog类 继承 Animal类
public class Dog extends Animal{
}
2.3.2 implements 关键字
使用 implements 关键字可以变相的使java具有多继承的特性,使用范围为类继承接口的情况,可以同时继承多个接口(接口跟接口之间采用逗号分隔)。
public interface A{
public void eat();
public void sleep();
}
public interface B{
public void show();
}
public interface C implents A,B{
}
2.3.3 super 关键字
我们可以通过super 关键字来实现对父类成员的访问,用来引用当前对象的父类。
在Java中使用super 来引用父类的成分,用this 来引用当前对象,如果一个类从另一个继承,我们new 这个子类的实例对象的时候这个子类对象里面会有一个父类对象。而这个父类对象的引用就需要使用super 关键字了,即super 是当前对象对父类对象的引用,而this 是当前对象自己的引用。
先给出一个super 关键字的案例,如下:
package cn.galc.test;
/**
* 父类
* @author gacl
*
*/
class FatherClass {
public int value;
public void f() {
value=100;
System.out.println("父类的value属性值="+value);
}
}
/**
* 子类ChildClass从父类FatherClass继承
* @author gacl
*
*/
class ChildClass extends FatherClass {
/**
* 子类除了继承父类所具有的value属性外,自己又另外声明了一个value属性,
* 也就是说,此时的子类拥有两个value属性。
*/
public int value;
/**
* 在子类ChildClass里面重写了从父类继承下来的f()方法里面的实现,即重写了f()方法的方法体。
*/
public void f() {
super.f();//使用super作为父类对象的引用对象来调用父类对象里面的f()方法
value=200;//这个value是子类自己定义的那个value,不是从父类继承下来的那个value
System.out.println("子类的value属性值="+value);
System.out.println(value);//打印出来的是子类自定义的那个value的值,这个值是200
/**
* 打印出来的是父类里面的value值,由于子类在重写从父类继承下来的f()方法时,
* 第一句话“super.f();”是让父类对象的引用对象调用父类对象的f()方法,
* 即相当于是这个父类对象自己调用f()方法去改变自己的value属性的值,由0变了100。
* 所以这里打印出来的value值是100。
*/
System.out.println(super.value);
}
}
/**
* 测试类
* @author gacl
*
*/
public class TestInherit {
public static void main(String[] args) {
ChildClass cc = new ChildClass();
cc.f();
}
}
画内存图了解程序的执行过程。分析任何程序都是从main() 方法的第一句开始分析的 ChildClass cc = new ChildClass(); 程序执行到这里时,首先在栈空间里面会产生一个变量cc,cc里面的值是什么这不好说,总而言之,通过这个值我们可以找到new出来的ChlidClass对象。由于子类ChlidClass是从父类FatherClass继承下来的,所以当我们new一个子类对象的时候,这个子类对象里面会包含有一个父类对象,而这个父类对象拥有他自身的属性value。这个value成员变量在FatherClass类里面声明的时候并没有对他进行初始化,所以系统默认给它初始化为0,成员变量(在类里面声明)在声明时可以不给它初始化,编译器会自动给这个成员变量初始化,但局部变量(在方法里面声明)在声明时一定要给它初始化,因为编译器不会自动给局部变量初始化,任何变量在使用之前必须对它进行初始化。
子类在继承父类value属性的同时,自己也单独定义了一个value属性,所以当我们new出一个子类对象的时候,这个对象会有两个value属性,一个是从父类继承下来的value,另一个是自己的value。在子类里定义的成员变量value在声明时也没有给它初始化,所以编译器默认给它初始化为0。因此,执行完第一句话以后,系统内存的布局如下图所示:
接下来执行第二句, cc.f() ; 当new一个对象出来的时候,这个对象会产生一个this的引用,这个this引用指向对象自身。如果new出来的对象是一个子类对象的话,那么这个子类对象里面还会有一个super引用,这个super指向当前对象里面的父对象。所以相当于程序里面有一个this,this指向对象自己,还有一个super,super指向当前对象里面的父对象。这里调用重写之后的f()方法,方法体内的第一句话:“super.f();”是让这个子类对象里面的父对象自己调用自己的f()方法去改变自己value属性的值,父对象通过指向他的引用super来调用自己的f()方法,所以执行完这一句以后,父对象里面的value的值变成了100。接着执行“value=200;”这里的vaule是子类对象自己声明的value,不是从父类继承下来的那个value。所以这句话执行完毕后,子类对象自己本身的value值变成了200。此时的内存布局如下图所示:
2.3.4 this 关键字
this关键字:this 是一个引用,是指向自己的引用。
假设我们在堆内存new了一个对象,在这个对象里面你想象着他有一个引用this,this指向这个对象自己,所以这就是this,这个new出来的对象名字是什么,我们不知道,不知道也没关系,因为这并不影响这个对象在内存里面的存在,这个对象只要在内存中存在,他就一定有一个引用this。
先给出一个this 关键字的案例,如下:
package cn.galc.test;
public class Leaf {
int i = 0;
public Leaf(int i) {
this.i = i;
}
Leaf increament() {
i++;
return this;
}
void print() {
System.out.println("i = " + i);
}
public static void main(String[] args) {
Leaf leaf = new Leaf(100);
leaf.increament().increament().print();
}
}
画内存图了解程序的执行过程。首先分析第一句话:Leaf leaf = new Leaf(100); 程序执行到这里的时候,栈空间里面有一个变量leaf,它指向了我们new出来的在堆空间里面的Leaf对象。new这个Leaf对象的时候,调用了构造方法Leaf(),这个构造方法里面有一个形参i,所以在栈空间里面给构造方法分配有一小块内存,名字叫i用来装传递过来的实参。这里传过来的实参是100,所以i里面装的值就是100。得到这个值之后,构造方法继续执行,执行this.i = i;这里就是把栈空间里面的i的值通过值传递给Leaf对象里面的成员变量i,所以成员变量i的值也变成了100。内存中的布局如下图所示:
构造方法执行完之后,为这个构造方法分配的内存消失,所以栈里面的i所标记的那一小块内存会消失。因此第一句话执行完之后,内存中的布局如下图所示:
接下来分析第二句话:leaf.increament().increament().print(); 首先逐个分析:leaf.increament(),这里是调用increament()方法,是对new出来的那个Leaf对象调用的,leaf是Leaf对象的引用对象,因此通过这个引用对象来调用increament()方法,即相当于是Leaf对象自己调用了increament()方法。increament()方法的定义如下:
Leaf increament(){
i++;
return this;
}
因此Leaf对象调用increament()方法时,首先执行方法体里面的第一句话i++;这样就把Leaf对象的成员变量i的值由原来的100变成了101。此时的内存布局如下图所示。
接下来执行方法体里面的第二句话:return this; 这里把this作为返回值,当有返回值的时候,首先会在栈里面给这个返回值分配一小块临时的存储空间。这块存储空间里面的内容是this里面的内容。this指向它自身,所以栈内存里面的那块临时存储空间里面装的this也是指向堆内存里面的Leaf对象。所以leaf.increament().increament().print();这句话里面的left.increament()这一小句话执行完之后,内存中的布局如下图所示。
leaf.increament().increament().print();这句话里面的left.increament()这一小句话执行完之后,返回一个this,此时leaf.increament().increament().print();就相当于是this.increament().print(); 接着栈里面的存储在临时空间里面的this调用increament()方法,而this指的就是Leaf对象,所以又是Leaf对象调用increament()方法。Leaf对象调用increament()方法时,又会执行方法体里面的i++,所以此时i又由原来的101变成了102。然后又执行return this,所以栈内存里面又多了一块临时存储空间,里面装的值也是this,这个this又是指向堆内存里面的Leaf对象。因此此时这个Leaf对象有了四个指向他自己的引用对象。leaf.increament().increament().print();这句话里面的leaf.increament().increament()这一小句话执行完之后,都返回了一个this,所以此时的leaf.increament().increament().print();就相当于是这样子的:this.this.print(); 接下来又是栈里面的那个新的this调用print()方法,使用this来调用,那就相当于是Leaf对象来调用,Leaf对象自己调用print()方法将自己的i属性的值打印出来,所以打印出来的结果应该是102。 因此main方法里面的整个程序执行完之后,内存中的布局如下图所示:
this的总结:this一般出现在方法里面,当这个方法还没有调用的时候,this指的是谁并不知道。但是实际当中,你如果new了一个对象出来,那么this指的就是当前这个对象。对哪个对象调用方法,this指的就是调用方法的这个对象(你对哪个对象调用这个方法,this指的就是谁)。如果再new一个对象,这个对象他也有自己的this,他自己的this就当然指的是他自己了。
2.3.5 final 关键字
final 关键字声明类可以把类定义为不能继承的,即最终类;或者用于修饰方法,该方法不能被子类重写:
-
声明类:
final class 类名 {//类体}
修饰符(public/private/default/protected) final 返回值类型 方法名(){//方法体}
-
声明方法:
注: 实例变量也可以被定义为 final,被定义为 final 的变量不能被修改。被声明为 final 类的方法自动地声明为 final,但是实例变量并不是 final
三. 多态
多态是面向对象的最重要特征之一,指允许不同类的对象对同一个消息作出响应,即同一消息可以根据发送对象的不同而采用多种不同的行为方式,其中发送消息就是调用函数。 eg.上图例中,不同类型的打印机打印图片的时候,所执行的操作是不一样的,所以最终的打印效果也不同。实现多态的技术称为动态绑定(dynamic binding),是指在执行期间判断所引用对象的实际类型,再根据其实际的类型调用与其相应的方法。
多态存在的三个必要条件:
1. 要有继承(两个类之间存在继承关系,子类继承父类)
2. 要有重写(在子类里面重写从父类继承下来的方法)
3. 父类引用指向子类对象
以上三个条件一旦满足,当你调用父类里面被重写的方法的时候,实际当中new 的是哪个子类对象,就调用该子类对象的方法(这个方法是从父类继承下来后重写后的方法)。
3.1 Java中多态的实现
3.1.1动态绑定,即多态。
先给出一个案例,如下:
package javastudy.summary;
class Animal {
/**
* 声明一个私有的成员变量name。
*/
private String name;
/**
* 在Animal类自定义的构造方法
* @param name
*/
Animal(String name) {
this.name = name;
}
/**
* 在Animal类里面自定义一个方法enjoy
*/
public void enjoy() {
System.out.println("动物的叫声……");
}
}
/**
* 子类Cat从父类Animal继承下来,Cat类拥有了Animal类所有的属性和方法。
* @author gacl
*
*/
class Cat extends Animal {
/**
* 在子类Cat里面定义自己的私有成员变量
*/
private String eyesColor;
/**
* 在子类Cat里面定义Cat类的构造方法
* @param n
* @param c
*/
Cat(String n, String c) {
/**
* 在构造方法的实现里面首先使用super调用父类Animal的构造方法Animal(String name)。
* 把子类对象里面的父类对象先造出来。
*/
super(n);
eyesColor = c;
}
/**
* 子类Cat对从父类Animal继承下来的enjoy方法不满意,在这里重写了enjoy方法。
*/
public void enjoy() {
System.out.println("我养的猫高兴地叫了一声……");
}
}
/**
* 子类Dog从父类Animal继承下来,Dog类拥有了Animal类所有的属性和方法。
* @author gacl
*
*/
class Dog extends Animal {
/**
* 在子类Dog里面定义自己的私有成员变量
*/
private String furColor;
/**
* 在子类Dog里面定义Dog类的构造方法
* @param n
* @param c
*/
Dog(String n, String c) {
/**
* 在构造方法的实现里面首先使用super调用父类Animal的构造方法Animal(String name)。
* 把子类对象里面的父类对象先造出来。
*/
super(n);
furColor = c;
}
/**
* 子类Dog对从父类Animal继承下来的enjoy方法不满意,在这里重写了enjoy方法。
*/
public void enjoy() {
System.out.println("我养的狗高兴地叫了一声……");
}
}
/**
* 子类Bird从父类Animal继承下来,Bird类拥有Animal类所有的属性和方法
* @author gacl
*
*/
class Bird extends Animal {
/**
* 在子类Bird里面定义Bird类的构造方法
*/
Bird() {
/**
* 在构造方法的实现里面首先使用super调用父类Animal的构造方法Animal(String name)。
* 把子类对象里面的父类对象先造出来。
*/
super("bird");
}
/**
* 子类Bird对从父类Animal继承下来的enjoy方法不满意,在这里重写了enjoy方法。
*/
public void enjoy() {
System.out.println("我养的鸟高兴地叫了一声……");
}
}
/**
* 定义一个类Lady(女士)
* @author gacl
*
*/
class Lady {
/**
* 定义Lady类的私有成员变量name和pet
*/
private String name;
private Animal pet;
/**
* 在Lady类里面定义自己的构造方法Lady(),
* 这个构造方法有两个参数,分别为String类型的name和Animal类型的pet,
* 这里的第二个参数设置成Animal类型可以给我们的程序带来最大的灵活性,
* 因为作为养宠物来说,可以养猫,养狗,养鸟,只要是你喜欢的都可以养,
* 因此把它设置为父类对象的引用最为灵活。
* 因为这个Animal类型的参数是父类对象的引用类型,因此当我们传参数的时候,
* 可以把这个父类的子类对象传过去,即传Dog、Cat和Bird等都可以。
* @param name
* @param pet
*/
Lady(String name, Animal pet) {
this.name = name;
this.pet = pet;
}
/**
* 在Lady类里面自定义一个方法myPetEnjoy()
* 方法体内是让Lady对象养的宠物自己调用自己的enjoy()方法发出自己的叫声。
*/
public void myPetEnjoy() {
pet.enjoy();
}
}
public class TestPolymoph {
public static void main(String args[]) {
/**
* 在堆内存里面new了一只蓝猫对象出来,这个蓝猫对象里面包含有一个父类对象Animal。
*/
Cat c = new Cat("Catname", "blue");
/**
* 在堆内存里面new了一只黑狗对象出来,这个黑狗对象里面包含有一个父类对象Animal。
*/
Dog d = new Dog("Dogname", "black");
/**
* 在堆内存里面new了一只小鸟对象出来,这个小鸟对象里面包含有一个父类对象Animal。
*/
Bird b = new Bird();
/**
* 在堆内存里面new出来3个小姑娘,名字分别是l1,l2,l3。
* l1养了一只宠物是c(Cat),l2养了一只宠物是d(Dog),l3养了一只宠物是b(Bird)。
* 注意:调用Lady类的构造方法时,传递过来的c,d,b是当成Animal来传递的,
* 因此使用c,d,b这三个引用对象只能访问父类Animal里面的enjoy()方法。
*/
Lady l1 = new Lady("l1", c);
Lady l2 = new Lady("l2", d);
Lady l3 = new Lady("l3", b);
/**
* 这三个小姑娘都调用myPetEnjoy()方法使自己养的宠物高兴地叫起来。
*/
l1.myPetEnjoy();
l2.myPetEnjoy();
l3.myPetEnjoy();
}
}
3.1.2 多态(动态绑定)的内存图
分析上例代码,先从main() 方法的第一句开始,Cat c = new Cat("Catname","blue"); 程序执行到这里,栈空间里面有一个变量 c ,c 里面装着一系列的值,通过这些值可以找到堆内存里面new 出来的 Cat 对象。因此 c 是 Cat 对象的一个引用,通过c 就能够看到Cat 对象的全部信息。c 指向new 出来的 Cat 对象,在new 这个Cat 对象的时候调用了Cat 对象的构造方法Cat(String n,String c),定义如下: 【“栈静堆动”】
Cat(String n,String c){
super(n);
eyesColor=c;
}
在构造子类对象的时候,首先使用父类对象的引用super 调用父类的构造方法Animal(String name),定义如下:
Animal(String name){
this.name=name;
}
因而会把传递过来的字符串“Catname” 交付给父类对象的name 属性。当Cat(String n,String c) 构造方法调用结束后,再真真正正地在堆内存中new 出一只Cat的实例对象,这只Cat里面包含有父类对象Animal,这个Animal对象有自己的属性name,name属性的值为调用父类构造方法时传递过来的字符串Catname。除此之外,这只Cat还有自己的私有成员变量eyesColor,eyesColor属性的属性值为调用子类构造方法时传递过来的字符串blue。所以执行完这句话以后,内存中的布局是栈内存里面有一个引用c,c指向堆内存里面new出来的一只Cat,而这只Cat对象里面又包含有父类对象Animal,Animal对象有自己的属性name,属性值为Catname,Cat除了拥有从Animal类继承下来的name属性外,还拥有一个自己私有的属性eyesColor,属性值为blue。这就是执行完第一句话以后整个内存布局的情况如下图所示:
接下来,再看这句话:Lady l1 = new Lady(“l1”,c);
程序执行到这里,首先在栈内存里面多了一个引用变量l1,l1里面装着一个值,通过这个值可以找到在堆内存里面new出来的Lady对象。l1就是这个Lady对象的引用,l1指向Lady对象。在创建Lady对象时,调用Lady类的构造方法:Lady(String name,Animal pet),其定义如下:
Lady(String name,Animal pet){
this.name=name;
this.pet=pet;
}
这个构造方法有两个参数,分别是String类型的name和Animal类型的pet,pet参数是一个父类对象的引用类型,这里把l1和c作为实参传递给了构造方法,接着在构造方法里面执行this.name=name,把传递过来的l1由传给Lady对象的name属性,因此Lady对象的name属性值为l1,这里也把前面new出来的那只Cat的引用c传递给了构造方法里面的参数pet,接着在构造方法里面执行this.pet=pet,pet参数又把c传过来的内容传递给Lady对象的pet属性,因此pet属性的属性值就是可以找到Cat对象的地址,因此Lady对象的pet属性也成为了Cat对象的引用对象了,通过pet里面装着的值是可以找到Cat对象的,因此pet也指向了Cat,但并不是全部指向Cat,pet指向的只是位于Cat对象内部的Animal对象,这是因为在调用构造方法时,是把c当成一个Animal对象的引用传过来的,把c作为一个Animal对象传递给了pet,所以得到的pet也是一个Animal对象的引用,因此这个pet引用指向的只能是位于Cat对象里面的Animal对象。在我pet引用对象眼里,你Cat对象就是一只普通的Animal,访问你的时候只能访问得到你里面的name属性,而你的eyesColor属性我是访问不到的,我能访问到你的name属性,访问的是位于你内部里面的父对象的name属性,因为我pet引用本身就是一个父类对象的引用,因此我可以访问父类对象的全部属性,而你子类对象Cat自己新增加的成员我pet引用是访问不了的。不过现在我pet引用不去访问你父类对象的成员变量name了,而是去访问你的成员方法enjoy了。首先是使用Lady对象的引用l1去调用Lady对象的myPetEnjoy()方法,myPetEnjoy()方法定义如下:
public void myPetEnjoy(){
pet.enjoy();
}
然后在myPetEnjoy()方法体里面又使用pet引用对象去调用父类对象里面的enjoy方法。
方法是放在代码区(code seg)里面的,里面的方法就是一句句代码。因此当使用pet引用去访问父类对象的方法时,首先是找到这个父类对象,然后看看它里面的方法到底在哪里存着,找到那个方法再去执行。这里面就比较有意思了,code seg里面有很多个enjoy方法,有父类的enjoy()方法,也有子类重写了从父类继续下来的enjoy()方法,那么调用的时候到底调用的是哪一个呢?是根据谁来确定呢?注意:这是根据你实际当中的对象来确定的,你实际当中new出来的是谁,就调用谁的enjoy方法,当你找这个方法的时候,通过pet引用能找得到这个方法,但调用代码区里面的哪一个enjoy方法不是通过引用类型来确定的,如果是通过引用类型pet来确定,那么调用的肯定是Animal的enjoy()方法,可是现在是根据实际的类型来确定,我们的程序运行以后才在堆内存里面创建出一只Cat,然后根据你实际当中new出来的类型来判断我到底应该调用哪一个enjoy()方法。如果是根据实际类型,那么调用的就应该是Cat的enjoy()方法。如果是根据引用类型,那么调用的就应该是Animal的enjoy()方法。现在动态绑定这种机制指的是实际当中new的是什么类型,就调用谁的enjoy方法。所以说虽然你是根据我父类里面的enjoy方法来调用,可是实际当中却是你new的是谁调用的就是谁的enjoy()方法。即实际当中调用的却是子类里面重写后的那个enjoy方法。当然,讲一点更深的机制,你实际当中找这个enjoy方法的时候,在父类对象的内部有一个enjoy方法的指针,指针指向代码区里面父类的Animal的enjoy方法,只不过当你new这个对象的时候,这个指针随之改变,你new的是什么对象,这个指针就指向这个对象重写后的那个enjoy方法,所以这就叫做动态绑定。只有在动起来的时候,也就是在程序运行期间,new出了这个对象了以后你才能确定到底要调用哪一个方法。我实际当中的地址才会绑定到相应的方法的地址上面,所以叫动态绑定。调这个方法的时候,只要你这个方法重写了,实际当中调哪一个,要看你实际当中new的是哪个对象,这就叫多态,也叫动态绑定。动态绑定带来莫大的好处是使程序的可扩展性达到了最好,我们原来做这个可扩展性的时候,首先都是要在方法里面判断一下这只动物是哪一类里面的动物,通过if (object instanceof class)这样的条件来判断这个new出来的对象到底是属于哪一个类里面的,如果是一只猫,就调用猫的enjoy方法,如果是一条狗,就调用狗的enjoy方法。如果我现在增加了一个Bird类,那么扩展的时候,你又得在方法里面写判断这只鸟属于哪一个类然后才能调用这只鸟的enjoy方法。每增加一个对象,你都要在方法里面增加一段判断这个对象到底属于哪个类里面的代码然后才能执行这个对象相应的方法。即每增加一个新的对象,都要改变方法里面的处理代码,而现在,你不需要再改变方法里面的处理代码了,因为有了动态绑定。你要增加哪一个对象,你实际当中把这个对象new出来就完了,不再用去修改对象的处理方法里面的代码了。也就是当你实际当中要增加别的东西的时候,很简单,你直接加上去就成了,不用去改原来的结构,你要在你们家大楼的旁边盖一个厨房,很简单,直接在旁边一盖就行了,大楼的主要支柱什么的你都不用动,这就可以让可扩展性达到了极致,这就为将来的可扩展打下了基础,也只有动态绑定(多态)这种机制能帮助我们做到这一点——让程序的可扩展性达到极致。因此动态绑定是面向对象的核心,如果没有动态绑定,那么面向对象绝对不可能发展得像现在这么流行,所以动态绑定是面向对象核心中的核心。
总之,动态绑定(多态):动态绑定是指在“执行期间”(而非编译期间)判断所引用的实际对象类型,根据其实际的类型调用其相应的方法。所以实际当中找要调用的方法时是动态的去找的,new的是谁就找谁的方法,这就叫动态绑定。动态绑定帮助我们的程序的可扩展性达到了极致。
3.1.3 方法重写(override)和 方法重载(overload)的区别
Java中多态的实现方式:接口实现、继承父类进行方法重写,同一个类中进行方法重载。
方法重写(override):子类与父类之间,在Java中子类可继承父类中的方法,而不需要重新编写相同的方法。但有时子类并不想原封不动地继承父类的方法,而是想作一定的修改,这就需要采用方法的重写。方法重写,又称方法覆盖。如果子类中的方法和父类中的某一个方法具有相同的方法名、返回类型、参数列表,则新方法覆盖原来继承于父类所得到的方法,如果只是原封不动地调用父类继承下来的方法,使用关键字super 即可。
规则:"两同两小一大":
1.“两同”:方法名相同,形参列表相同;即方法签名相同
2.“两小”:指的是子类方法返回值类型与方法声明抛出的异常类要比父类方法更小或相等,因为子类可以解决父类的一些问题,不能比父类有更大的问题;列如,父类的一个方法声明了一个检查异常IOException,在重写这个方法时就不能抛出Exception,只能抛出IOException的子类异常。
3.“一大”:指的子类方法的访问权限应该比父类的更大或相等;
注意:
1.方法的重写,在一般情况下,返回自类型、方法名、形参列表、访问权限、抛出的异常,都应该保持一致;
2.被覆盖的方法不能为private ,否则在其子类中只是定义了一个方法,并没有对其进行覆盖。
案例:
class Person{
private String name; //私有化属性
private int age;
public Person(String name, int age) { //构造方法
this.name = name;
this.age = age;
}
public String getName() { //与私有化属性对应的get 方法
return name;
}
public void setName(String name) { //与私有化属性对应的set 方法
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public void ShowInfo(){ //父类中要被重写的方法,其访问修饰符不能为private
System.out.println("name:"+name+"and age:"+age);
}
}
class Student extends Person{
private String school; //子类自己的私有化属性
public Student(String name, int age, String school) {
super(name, age); //子类的构造方法,使用super关键字调用父类的构造方法
this.school = school; //添加自己的私有化属性
}
public String getSchool() {
return school;
}
public void setSchool(String school) {
this.school = school;
}
public void ShowInfo(){ //子类重写冲父类继承来的某一个方法
System.out.println("name:"+name+"and age:"+age+" and school:"+school);
}
}
public class TestOverWrite { //测试类
public static void main(String[] args) {
Person aPerson = new Person("zhaoguohao", 20); //调用父类的构造方法,创建出一个新的实例对象
aPerson.ShowInfo(); //调用父类的ShowInfo()方法
Student aStudent = new Student("zhaoguohao",20,"JLU"); //调用子类中重写过的构造方法,创建实例对象
aStudent.ShowInfo(); //调用子类重写过的ShowInfo()方法
}
}
方法重载(overload):在同一个类中,Java允许同一个类里定义多个同名的方法,只要形参列表不同即可。如果一个类中包含了两个或两个以上的方法名相同,但参数列表不同,则被称为方法重载。eg.比如:int max(int a , int b)方法与void max(int a, int b)方法是不构成重载的,它们是重名的两个方法,在一个类中声明两个重名的方法是不允许的,编译会出错。
规则:“两同,一个不同”:
1.“两同”:在同一个类中,方法名称相同。方法的重载只跟方法名、参数列表有关,与方法的其它部分,如:返回值、修饰符无关
2.“一个不同”:形参列表不同,即紧跟方法名后的参数列表里面的参数个数不同,或者参数的数据类型不一样
注意:方法重载中,返回值类型可以相同也可以不同,也可以有不同的访问修饰符,可以抛出不同的异常
案例,构造方法的重载:
package cn.galc.test;
public class Person {
int id;
int age;
public Person() { //默认的无参数构造方法,如果方法体重没有其它的构造方法
id=0; //隐式无参构造方法其作用,如果有写其它的构造方法,默认的隐式无参构造方法会消失掉,
age=20; //显式无参构造方法是不会消失的,与其它的构造方法之间形成方法的重载
}
public Person(int i) { //构造方法重载一
id=i;
age=20;
}
public Person(int i,int j) { //构造方法重载二
id=i;
age=j;
}
}
------------------------------------------------------------------ 我是低调的分隔线 ----------------------------------------------------------------------
吾欲之南海,一瓶一钵足矣...