本文目录:【蓝色部分为本章的目录】
1.基本概念
2.Java变量相关
1)Java变量分类
2)Java中变量的初始化
3)Java变量修饰符和访问域
4)Java类修饰符[不包含内部类]
3.Java涉及OO的关键知识点【主体】
1)继承的基本概念
2)抽象类、接口、final类:
3)重载和重写:
4)对象的拷贝[深拷贝和浅拷贝]:
5)关键字this、super
6)Java中的inlining[内联]
7)带继承的构造函数以及构造顺序
8)谈谈Object中的方法:equals、hashCode、toString
9)带继承的类型转换以及转换中关于成员变量和成员函数的调用
10)Java语言中的反射
11)按引用传递和值传递原理
12)Java中的包和导入
13)匿名类和内部类
4.Java编程OO设计技巧
1)对象创建以及周期
2)对象属性设置
3)垃圾回收
4)继承、接口、抽象类
5.总结
1.基本概念:
类和对象:OO里面什么称为类,类一般性我们定义为创建对象的一个蓝图,当我们根据某个类创建了一个对象的时候,我们就说该对象是这个类的一个实例(instance),类描述了某些对象的基本结构,是对对象的相关属性的一种定义,它定义了对象所具有的属性、方法、事件等各种基本点。
类设计的关键概念:封装是对象在工作的时候的一个关键概念,也是我们在进行OO设计的时候的一个需要掌握的点,封装简单讲就是将对象内部的一些细节实现隐藏起来不对外公布,仅仅对外公布某个对象能够提供的操作结果,从而实现信息隐藏的目的。在封装过程,对象内的数据我们一般称为成员变量(instance fields),对象内针对这些数据执行的操作我们可以叫做操作方法(成员函数)(methods),一个对象这些属性的集合所反映出来的就是该对象的状态。
在Java里面,所有的复合数据都是从Object类继承而来的,这一点可以通过使用Java反射去获取父类的名称来获得。
对象:在OO设计里面,对象有几个需要掌握的基本点:
对象的行为:对象的行为指代的是这个对象能够做什么,不能做什么,包括这个对象有哪些接口是对外提供了我们可以直接进行使用等相关概念。
对象的状态:对象的状态指代的是在程序运行过程,某个对象实例的一些属性发生了相关的变化过后产生的一个对象状态,在ORM设计中我们经常会遇到对象状态的操作。
对象的标识:对象的标识指代的是这个对象如何区别于其他对象,即是是同一个类产生的新实例,其本质上讲它们属于两个不同的对象,而不应该是同一个对象。
类与类的关系:
依赖(Dependence):依赖关系简单讲就是users-a的关系,两个类创建的对象实例本身没有任何关系,在编程过程反映出来的就是两个对象在相互之间的调用关系,比如某个对象在执行它自己的行为的时候调用了其他对象的方法这些相关操作都属于users-a的基本关系,或者说在某个Class里面使用了其他Class来定义成员变量。
组合(Aggregation):组合关系可以认为是has-a的关系,这种关系的两个类存在一个包含关系,很容易理解就是比如某个类包含了另外一个类,这种概念严格上讲不在操作,主要是成员变量,比如一个用户有个姓名属性成为NameInfo,然后这个NameInfo定义为一个包含了两个String的对象,一个是FirstName,另外一个是LastName,这种情况可以认为该对象本身和NameInfo之间的关系是has-a的关系。
继承(Inheritance):继承关系属于is-a的关系,这种关系意味着两个类存在父类(superclass)和子类(subclass)的概念,Java里面使用extends和implements两个关键字来体现两个类的集成关系。
(上边这些概念基本上在各种OO语言学习的开篇都会遇到,没什么特殊的,所以熟悉的人可以不用去了解)
Java中的Class:
预定义Class:在Java里面,什么称为预定义Class,学过Java的人都知道,JVM会在我们编写java程序的时候默认载入包java.lang,而java.lang里面已经存在了很多Class,这种Class就属于预定义的Class;不仅仅如此,以及Java API里面提供的Network相关类、IO相关类、XML相关类,这些可以不需要我们自定义,只需要import过后就可以直接使用的类就属于预定义的类。预定义类的各种使用方法可以参考JDK的API文档,里面针对目前存在的很多类都有详细的描述信息,有些类还提供了我们所需要的相关使用Demo可做参考。
一些代码的使用例子。
自定义Class:如果我们要使用自己的Class就需要在Java中自己定义Class,定义语法如下:
class UserInfo{
……
}
使用该语法我们就可以定义一个属于自己的Class,而在Class里面我们就可以根据自己的需求来设计相关成员变量和成员函数以及其他相关信息。
2.Java变量:
1)Java中的变量分为四种:类变量(又称为静态变量)、实例变量、局部变量、块变量
类变量——类变量只能在一个class域里面进行定义,同样只能在类里面使用,使用关键字为static关键字,这种变量可以直接通过Class.VAR的方式来调用,而且不需要对象实例存在就可以调用,它的执行原理在于当JVM的类加载器将类加载进来的时候,这个变量就已经存在的,而且可以使用了。定义方式和调用方式如下,下边的A就属于类变量:
class UserInfo{
static int A;
public static void main(String args[])
{
System.out.println(UserInfo.A);
}
}
实例变量——实例变量也是在class域里面进行定义,但是只能在Object域里面使用,不使用关键字static,这种变量不能像类变量一样用Class.VAR的方式来调用,这种变量使用条件就是这个变量所属的实例必须存在,在实例初始化过后这个变量就可以调用了,通过实例的方式进行调用,定义方式和调用方式如下,下边的a就属于实例变量:
class UserInfo{
int a;
public static void main(String args[])
{
UserInfo user = new UserInfo();
System.out.println(user.a);
}
}
局部变量——局部变量不能在class域里面进行定义,只能在函数域里面定义和使用,这种变量使用范围仅仅在某个局部块里面,即每个函数中定义的局部变量只能在函数本身中使用,其定义方式和调用方式如下,在下边代码里面,a就属于局部变量:
class UserInfo{
public static void main(String args[])
{
}
public static void f(int i)
{
int a = 0;
}
}
块变量——块变量只能在块域中进行定义,同样只能在块中使用,不能越过块域,其定义方式和调用方式如下,在下边代码里面,a变量就属于块变量:
class UserInfo{
public static void main(String args[])
{
{
int a = 0;
}
}
}
2)变量的初始化:
Java中的变量初始化规则是这样的:类变量和实例变量在没有进行显示初始化的时候,系统会自动赋予默认值,这里需要补充的是所有继承于Object的默认值为null,块变量和局部变量必须进行显示赋值操作,否则无法通过编译器的编译。这一点用下边的代码段来说明:
class UserInfo{
int a;
public static void main(String args[])
{
int b;
System.out.println(b);//这句话无法通过编译器的编译,因为是局部变量,没有进行显示初始化操作
System.out.println(a); //这句话的输出为0,因为系统自动初始化了一个默认值
}
}
Java中有一种初始化的方式称为静态初始化,静态初始化会存在于class域的一个static块语句里面,所有静态初始化的语句会在类加载的时候第一时间执行,所以看下边这句代码:
class UserInfo{
static int a = 0;
static{
System.out.println("Init static");
}
public static void main(String args[])
{
System.out.println("Init main");
}
}
上边代码运行一次可以知道,静态初始化块是最先执行的,输出会如下:
Init static
Init main
这种静态初始化的方式在我们使用JDBC的时候我们会经常用来进行驱动的加载,而且它还有一个特性是:它只运行一次!其含义在于,及时以后使用该类去创建实例,这一段再也不会执行,因为静态初始化属于Class域的操作,仅仅在类第一次被JVM加载的时候会执行,以后就不会再执行了。
3)Java变量修饰符和访问域(变量相关):
在面向对象里面,我们设计的时候需要实现信息隐藏,同理而言,就是我们在设计类的属性和对应的成员函数的时候需要使用变量和函数的修饰符来操作,在Java语言里面,总共分为四种访问域:
public域:public域使用public关键字来实现,该域里面的内容是任何成员都可以访问的,访问前提是已经导入了该类所在的包
protected域:protected域使用protected关键字来实现,该域里面的内容访问限制在于:访问该类的类必须和被访问类在同一个包里面,或者访问类是被访问类的子类,这两种关系满足protected域的访问规则
default域:default域不需要用任何关键字来实现,直接在类里面定义实例变量或者类变量的时候不带任何修饰符,该域的访问规则是:访问类和被访问类必须在同一个包里,因为这个原因,default域又可以称为包域
private域:private域使用private关键字实现,访问规则为:该关键字定义的东西只有该类本身可以访问,其他类以及子类都不可以访问该成员
按照以下的表可以知道这几个修饰符之间的大小和相互之间的访问关系
所有成员 | 子类成员 | 同一个包 | 本类成员 | |
public | 可访问 | 可访问 | 可访问 | 可访问 |
protected | 不可访问 | 可访问 | 可访问 | 可访问 |
default | 不可访问 | 不可访问* | 可访问 | 可访问 |
private | 不可访问 | 不可访问 | 不可访问 | 可访问 |
*注意:这里需要注意的是,在同一个包里面的protected域和default域中,需要掌握的规则为:
[1]protected域本身是为了对象继承而设计的,所以只要满足继承关系的时候protected是可以访问的,而不去管两个类是否在同一个包内
[2]default域本身就是包域,所以上边的表格里面写的不可访问前提条件为子类成员不在同一个包内,即如果子类成员在同一个包内,default域是可以访问的,谨记:default域为包域
4)Java类修饰符[不包含内部类]
Java里面外部类修饰符只有两种:公共类(使用public关键字)、友好类(不使用任何关键字)
1.公共类只要被导入过后,任何一个类都可以访问该类,至于类中的成员按照变量访问域来规范,前提是你要可以访问该类才能访问类中的成员
2.友好类只能在同一个包里面访问,其他包里面的类即使导入了该包也只能访问该包里面的共有类,这个符合变量的包域即default域
3.在我们编程过程中,.java文件对应的就是一个class的类名称,当然你可以把一个包里面所有的类都用default修饰,不可排除这种做法完全是在脑残,一个.java文件里面最多拥有一个public修饰的class,而且被public修饰的class必须和.java源文件同名,这是Java语言本身规定的
3.Java涉及OO的一些关键知识点:
1)继承基本概念:
在Java语言里面,如果要实现类的继承有两种方式:类继承(extends)和接口继承(implements),一般情况下,extends用来描述is-a关系,而implements用来描述has-a关系。使用extends关键字标识一个类派生于另外一个类,习惯上,我们把被派生的类成为超类,也就是父类,而派生的类称为子类,根据OO里面多态法则可以知道,子类会具有比超类更多的数据和功能。当一个子类继承了某个父类的时候子类就拥有了超类的域和方法,简单来说,继承看上去类似复制操作。Java中所有的类都直接或者间接继承于Object类,因此可以认为Object是所有Java类的超类。
这两种继承有几个需要注意的点:
【1】类继承为单继承关系,一个类只会存在一个父类,不会有第二个,可以理解为父类的唯一性:一个类只可以继承于另外的一个类
【2】但是接口继承为多继承关系,当一个类实现一个接口的时候,可以同时实现多个接口:一个类可以实现多个接口
【3】接口本身支持多继承关系,其意思为一个接口本身可以从另外几个接口继承过来,接口继承的时候,只能使用extends关键字,不能使用implements关键字来完成接口的继承,字面意思上讲:接口不可以实现接口。
接下来再看一段代码,以下都为合法的写法:
interface AA{}
interface CC{}
interface BB extends AA,CC{} //这是一个接口继承于另外一个接口
class A{}
class B extends A{} //类的单继承
class C extends A implements AA,CC{} //类继承于某个父类而且同时实现多个接口操作,在继承和接口实现同时出现的时候,实现应该写在继承之后
继承(extends)和实现(implements)的规则如下:
extends:子类可以继承父类除开修饰为private的所有属性和方法,当子类需要调用父类的方法或者属性的时候使用super关键字,当子类继承超类的方法的时候,方法修饰的域只能扩大不能缩小,父类里面被标记为final的成员函数同样是不能继承的
implements:子类实现了某个接口过后,必须实现接口中所有的抽象方法[接口中的方法默认全就是抽象的],所有的接口实现方法都必须写出它的实现过程
2)抽象类、接口、final类:
i抽象类——使用abstract作为类的修饰符,一旦被abstract修饰的类就为抽象类,抽象类表示该类里面可以仅仅包含一些方法的定义,可以不需要该方法的实现。抽象类里面可以定义抽象方法,使用abstract修饰符进行定义,其使用方法为:
[1]在具体类继承抽象类时,必须覆盖该类里面的每一个抽象方法,而每个已实现的方法对应的方法签名和抽象类制定的方法必须一样
[2]当父类已经有实际功能的方法时,该方法在子类中可以不实现,直接引用该方法就好,但是子类也可以重写该父类的方法
[3]抽象类不可以实例化,但是并不代表抽象类不可以有构造函数
[4]当抽象类继承抽象类时,可以不用完全覆盖该类里面的每个抽象方法,也可以实现一部分,或者完全不去实现都可以
这里需要提醒一点就是:可以理解为抽象类的定义本身设计是为了让别的类继承该类而设计的,而在别的类继承抽象类的过程中,直到该抽象类里面定义的所有抽象方法都实现了才能使用该具体类,有一条法则必须牢记:当一个类里面的方法为抽象方法时,该类一定是抽象类,当一个类是抽象类时,里面的方法不一定是抽象方法或者说这个类里面不一定包含了抽象方法。
先看一段代码来理解整个流程:
abstract class BaseUser{
//此方法为抽象类里面的具体方法
public void helloImpl(){ System.out.println("Hello"); }
//此种方法定义方式为抽象类里面抽象方法的定义方式,直接在函数后边使用“;”而不带任何函数体
public abstract void helloAbstract();
public abstract void helloSecondAbstract();
}
/**
*UserOne类继承于抽象类BaseUser,尽管UserOne本身也属于抽象类,
*但是该类已经实现了BaseUser里面的所有方法,所以:抽象类里面不一定包含了抽象方法
*/
abstract class UserOne extends BaseUser{
public void helloAbstract(){……}
public void helloSecondAbstract(){……}
}
/**
*UserTwo类继承于抽象类BaseUser,UserTwo也属于抽象类,所以该类里面
*还可以继续保留抽象方法helloAbstract(),这个类出现的目的和抽象类本身目的一样为了让其他类来继承
*/
abstract class UserTwo extends BaseUser{
//其实这一行在这个类里面可以不写,因为如果不实现该方法,这种定义已经从父类继承过来了
public abstract void helloAbstract();
public void helloSecondAbstract(){……}
}
/**
*UserThree为具体类,之所以它仅仅实现一个方法helloAbstract()是因为它不是从BaseUser继承
*它从实现了helloSecondAbstract()方法的类UserTwo继承过来
*/
class UserThree extends UserTwo{
public void helloAbstract(){……}
}
/**
*UserFour为一个具体类,它直接从BaseUser抽象类继承过来,所以该类实现了里面的所有抽象方法
*/
class UserFour extends BaseUser{
public void helloAbstract(){……}
public void helloSecondAbstract(){……}
}
ii接口——接口可以理解为一个特殊的抽象类,在Java里面不使用abstract来定义,直接使用interface关键字定义,对于interface特性的掌握,通过比较的方式来进行,下边为抽象类和接口的区别:
[1]抽象类里面可以拥有具体的方法实现,接口不可以有,接口里面所有的方法都是抽象方法
[2]接口没有构造函数,而抽象类拥有一个系统默认的无参数构造函数
[3]接口没有内置的this和super变量,而抽象类里面拥有这两个内置变量
[4]接口的成员变量一定是常量,我们又称为接口常量,而abstract类的成员变量就是一般变量
[5]接口的成员函数的修饰符均为public,而抽象类的成员函数的修饰符不做限制
先看一段代码:
abstract interface A{
//接口常量的定义,别忘记了需要初始化
public static final int A_INT = 0;
//接口中的接口方法的定义,这方法默认就属于抽象方法
public abstract void fun();
}
在上边代码段里面,定义了一个接口A,而灰色部分内容是可写可不写的,因为灰色部分写不写的效果是一样的,上边代码段的定义和下边这段是等价的:
interface A{
int A_INT = 0;
void fun();
}
一般情况都是用以上简化的定义方式,其意思可以这样理解:只要我们在定义接口的时候语法没有错,那么JVM会为该接口自动赋对应的接口修饰符,使得接口定义为第一种方式,所以二者等价
iii.final关键字——final关键字一般有以下几种用法:
[1]当final用于变量的时候,如果该变量为一个基础变量,即《Java基本数据类型》里面讲到的八种基本变量时,表示该变量存在内存里面的值为一个常量,不可更改
[2]当final用于一个方法的时候,表示该方法不可以被继承,即使该方法为public修饰符,这个方法也不可以被子类继承,只能被子类调用
[3]当final用于一个类定义的时候,表示该类不可以有子类,这里可以引申一点:abstract和final两个关键字是不可以同时出现的
[4]当final用于变量的时候,如果该变量为一个符合变量,即Java中继承于Object的变量,表示该引用不可以指向其他的对象,但是并不代表该引用指向的对象不可更改
3)重载和重写:
i重载(Overloading)——方法重载是让类以统一的方式处理不同类型数据的一种手段,Java里面的方法重载,就是在类中可以创建多个方法,它们具有相同的名字,但是却具有不同的参数列表和不同的定义,调用该方法的时候,通过使用参数的类型和个数来判断需要调用哪个函数,这个地方也体现了类里面的针对方法的多态设计,在方法重载里面,需要注意的有几点:
[1]在使用重载的时候,只能通过不同的参数表,比如:不同的参数类型、不同的参数个数、不同的参数顺序,来区别二者是否重载
[2]不能通过访问权限、返回值、抛出的异常来进行函数重载
[3]方法的异常类型和数目对重载本身没有影响
[4]针对Java中的继承而言,如果某个方法在父类中的修饰符是private,那么就不能在子类中对其进行重载,如果定义的话,子类是定义了一个新方法,不会达到重载效果
还有一点需要注意的是:函数重载可能会遇到几个特殊情况,比如实参转型、返回值转型、不定参数重载【JDK 5.0以上】,这三种情况在考虑的时候需要详细检查一下,基本规则就和Java语言的转型规则是一样的。
ii重写(Overriding)——Java语言中,子类可以继承父类中的方法,而不需要重新编写该方法,但是有时候子类可能会在父类同样的方法上做一定的修改,这样就需要采用方法重写,方法重写有时候可以定义为覆盖。如果子类中的方法签名与父类中的方法签名一模一样,则该方法就会覆盖父类中的方法,若要调用父类中的方法,可以使用super关键字。
在方法重写中需要注意几个关键的内容:
[1]覆盖的方法签名必须和被覆盖的方法的签名完全匹配,才能达到覆盖的效果
[2]覆盖的方法的返回值也必须和被覆盖的方法的返回值是一致的
[3]覆盖方法所抛出的异常和被覆盖的方法抛出的异常一致,或者是其子类异常才可以
[4]若父类中的方法为private,在子类中使用覆盖的时候是没有效果的,这个时候子类只是定义了一个新方法,却没有进行覆盖操作
接下来看几段比较不容易理解的典型的代码【普通代码省略】:
class A{
private final void A(){
System.out.println("Super Hello");
}
}
class B extends A{
private final void A(){
System.out.println("Sub Hello");
}
}
以上代码里面,没有出现覆盖,原因是父类的private方法本身是不可以被继承的,覆盖满足的条件必须是子类继承了父类的方法才能够被新的定义覆盖,上边代码段子类只是定义了一个新方法,不存在覆盖现象。由此可知,上边的代码是可以通过编译的,而且使用的时候不会出错。
class A{
private void A(){}
}
class B extends A{
/**这句话可以编译通过,但是这里并没有重
*载父类的方法,所以针对A类而言不存在函数重载现象
**/
public void A(int a){……}
/**这句话也可以编译通过,按照重载的法则,仅仅包含了不同
*的返回值是不能算重载的,这种情况是没有办法通过编译的,但是这句话可以通过编译
*原因在于父类的函数修饰符为private,所以这里并没有重载,而这里重载的函数只是
*子类的A(int)函数,而且这里不存在重写,所以这段代码是完全符合逻辑的一段代码
**/
public int A(){……}
}
从整体意义上讲,上边代码仅仅出现了一次重载操作,即是子类本身进行了重载,因为A函数的参数签名不一样,一个参数是int,一个是空参数
4)对象的拷贝[深拷贝和浅拷贝]:
i.关于clone[对象拷贝]——在实际编程过程,有时候我们会遇到一种情况:当你有一个对象A,在某一个时刻,A已经保存了对应的属性值,而且这些值本身是有效的,这个时候可能需要一个和A完全相同的对象B,并且当B里面的属性值发生变化的时候,A中的属性值不受影响,可以理解为A和B独立,但是B的初始化不是按照我们平时创建该对象的时候的初始化操作,B的初始化数据完全来自A。对Java存储模型了解的人都明白,在Java里面如果针对两个对象引用采取赋值操作的时候,仅仅是让两个引用指向了同一对象,如果其中一个引用里面的对象属性改变的时候会影响另外一个对象属性跟着改变,所以Java语言本身的对象赋值语句是不能完成上边的需求的。在这种时候,就需要用到Object类里面的通用方法clone(),这里需要说明的是:
通过clone()方法创建的对象是一个新对象,它可以认为是源对象的一个拷贝,但是在内存堆中,JVM会为这个拷贝分配新的对象存储空间来存放该对象的所有状态
该拷贝和普通对象使用new操作符创建的对象唯一的区别在于初始值,这个拷贝的初始值不是对象里面成员的默认值,而是和源对象此刻状态的成员的值是一样的
下边这段代码是clone方法的运用:
public class Testing {
public static void main(String args[]){
AClass class1 = new AClass();
class1.a = 12;
AClass class2 = (AClass)class1.clone();
System.out.println(class2.a);
System.out.println(class1==class2);
}
}
class AClass implements Cloneable{
public int a = 0;
public Object clone(){
AClass o = null;
try{
o = (AClass)super.clone();
}catch(CloneNotSupportedException ex){
ex.printStackTrace();
}
return o;
}
}
上边这段代码运行结果输出为:
12
false
可以知道的就是成功复制了一个AClass的对象,该对象的引用为class1,而拷贝对象的引用为class2,这两个引用通过==比较输出为false,证明这两个引用不是指向了同一个对象,而且拷贝对象里面的a的值和class1引用的对象里面的a的值是一样的,都是12,这样就成功完成了对象的拷贝过程。若你对上边这段代码还有不了解的地方可以尝试将下边的代码修改掉:
AClass class2 = (AClass)class1.clone() 修改为:AClass class2 = class1;
改了过后,输出结果应该为:
12
true
所以在对象的clone过程,需要注意的几点有:
[1]希望能够提供对象clone功能的类必须实现Cloneable接口,这个接口位于java.lang包里面
[2]希望提供对象clone功能的类必须重载clone()方法,在重载过程可以看到这句话:super.clone();也就是说,不论clone类的继承结构如何,我们在对象拷贝的时候都直接或简介调用了Object的clone()方法。而且细心留意可以看到Object的clone方法是protected域的,也就是说这个方法只有Object的子类可以调用,而在重载的时候将clone方法修饰符改为public
[3]还有一点很重要就是Object源代码里面的clone()方法是native方法,一般而言,对JVM来说,native方法的效率远比java中的普通方法高,这就是为什么我们在复制一个对象的时候使用Object的clone()方法,而不是使用new的方式。
[4]Cloneable接口和我们在编写IO程序的时候序列化接口一样,只是一个标志,这个接口是不包含任何方法的,这个标志主要是为了检测Object类中的clone方法,若我们定义的类想要实现拷贝功能,但是没有实现该接口而调用Object的clone方法,那么就会出现语句中catch块里面的异常错误,抛出CloneNotSupportedException。
ii浅拷贝——在对象clone的过程中,浅拷贝又称为“影子clone”,先看一段代码:
//这里先定义一个类
class AClass{
public int a;
public AClass(int a){ this.a = a;}
public void change(){ a += 12;}
public String toString(){ return "A Value is " + this.a;}
}
//定义一个clone类,里面包含了AClass的对象引用
class BClass implements Cloneable{
public int a = 12;
public AClass obj = new AClass(11);
public Object clone(){
BClass object = null;
try{
object = (BClass)super.clone();
}catch(CloneNotSupportedException ex){
ex.printStackTrace();
}
return object;
}
}
public class TestClone {
public static void main(String args[]){
BClass class1 = new BClass();
class1.a = 15;
System.out.println(class1.a);
System.out.println(class1.obj);
BClass class2 = (BClass)class1.clone();
class2.a = 22;
class2.obj.change();
System.out.println(class1.a);
System.out.println(class1.obj);
System.out.println(class2.a);
System.out.println(class2.obj);
}
}
运行上边这段代码会有以下输出:
15
A Value is 11
15 //这里拷贝成功了
A Value is 23 //!!!不对,根本没有调用class1里面的obj的change方法,所以不应该修改class1里面的obj里面的变量a的值【初衷】
22
A Value is 23
不知细心的读者有没有发现输出和我们预期的拷贝不一样,虽然class2引用的对象是从class1拷贝过来的,class2里面的引用obj和class1里面的引用obj实际上还是指向了同一个对象,其含义在于,拷贝的初衷是要复制一个一模一样的对象,包括对象里面的对象也应该实现的是复制操作,它最终的目的是保证class1和class2本身的属性以及class1和class2里面的对象引用的属性在拷贝过后的各种相关操作里面相互独立,上边输出证明了class1和class2里面的变量a确实已经拷贝成功,但是class1和class2里面的AClass对象的引用obj在拷贝过后还是指向了同一个对象,所以拷贝结束过后,调用class2的obj的change方法的时候,也修改了class1里面的obj指向的对象里面的值。所以在Java里面我们把上边的拷贝过程称为“浅拷贝”,同样又称为“影子clone”。
从这里可以知道,在JVM的对象复制里面,实际上基本数据类型可以直接通过这种方式来进行拷贝工作,而非原始类型这样操作了过后拷贝的对象仅仅拷贝了对象里面的基本数据类型的成员变量,而比较复杂的类型的成员变量并没有像预期一样产生拷贝效果,这种拷贝我们就称之为“浅拷贝”。
iii.深拷贝——如果要实现我们预期的对象拷贝效果,就需要使用深拷贝操作,其实在浅拷贝基础上实现深拷贝有两个步骤,以上边的代码为例:
[1]第一步:让AClass实现同样的clone功能
[2]第二步:在BClass的clone操作中多写入一句话:object.obj = (AClass)obj.clone();
【修改过后的代码,在这里就不写了,请读者自己去尝试深拷贝的改动,至于String和StringBuffer有关拷贝的特性,我将留到Java复合数据类型的讲解里面来详解】
5)关键字this、super
在Java语言的类定义中,有两个内置对象,一个是this,一个是super,这两个对象的含义如下:
this是一个指向对象自己的引用,该引用指向的对象为该类实例化的对象本身;
super是一个指向该类父类的引用,如果该类不继承于任何类,那么该类调用super引用的就是Object类
这里用一小段代码说明:
class AClass{
public AClass(int a){
System.out.println("A Init "+ a);
}
public void fun(int a){
System.out.println("fun int "+a);
}
}
class BClass extends AClass{
private int a = 24;
private static int b = 11;
public BClass(){
this(12);
//this.b = 12;
super.fun(12);
}
public BClass(int a)
{
super(12);
System.out.println("B init " + this.a);
}
}
针对以上代码段做几个详细的说明:
super.fun(12):在子类中访问父类的成员的时候使用super关键字,但是super不能访问父类的private成员;
super(12):子类如果要调用父类的构造函数,可以直接使用super(param)这中调用方式,但是如果是调用父类的构造方法,是不能够在子类的其他函数块里面进行的,只能在构造函数中进行,而且一般不能和this(param)同时出现,关于它们在构造函数中的使用,将在小节7中介绍;
//this.b=12:虽然这句话注释掉了,但是这句话是可以通过JVM编译的,这里需要区分的是在static块中调用非static成员和在普通块中调用static成员,在普通语句中调用static成员是可以使用this.VAR的方式进行操作的,但是一般不提倡这样做,因为一般调用static成员在编程的时候都是使用独一无二的方式Class.VAR,而不使用this.VAR。
this(12)/this.a:this在调用自己的成员的构造函数的时候,直接使用this(param)方式调用构造函数,如果是调用普通的成员就使用后者。
*:this和super都不能放在static块中运行,不论是static的静态初始化块,还是static的静态方法块,里面都不能出现this和super的关键字,这个道理很简单:static块是Class域的,就是说static块里面的内容是在JVM的类加载器第一次加载类的时候就被初始化了,而且整个过程只会初始化一次。而this和super都是Object域的操作,this指代的是实例本身,而不是类,同样的super指代的是父类的实例本身,也不是类,所以this和super是不能够出现在static的静态方法块里面的。其实从概念上讲,static和this、super操作的对象不一样,static是属于Class的,而super和this是属于Object的,所以编程过程必须注意这点。【当然很多人使用Eclipse或者Netbeans,IDE会在错误代码中出现提示的!】
(未完待续)