淘先锋技术网

首页 1 2 3 4 5 6 7

对象

面向对象方式通过向程序员提供表示问题空间(问题存在的地方,如某个需求)中的元素的工具而更进一步。这种表示方式非常通用,使得程序员不会受限于任类型的问题。我们将问题空间中的元素及其在解空间(问题建模的地方,如计算机)表示称为“对象”。(你还需要一些无法类比为问题空间元素的对象。)这种思想的实质是:程序可以通过添加新类型的对象使自身适用于某个问题。

什么是对象

Alan Kay曾经总结了第一个成功的面向对象语言、同时也是Java所基于的语言之一 的Smalltalk的五个基本特性,这些特性表现了一种存粹的面向对象程序设计方式:

  1. 万物皆为对象:将对象视为奇特的变量,它可以存储数据,除此你还可以要求在自身上执行操作。理论上讲,你可求解任何概念化构件(狗、建筑物、服务等),将其表示为程序中的对象
  2. 程序是对象的集合,它们通过发送消息来告知彼此所要做的。要想请求一个对象,就必须对该对象发送一条消息。更具体地说,可以把消息想像为对某象的方法的调用请求。
  3. 每个对象都有自己的由其他对象所构成的存储。换句话说,可以通过创建包含现有对象的包的方式来创建新类型的对象。因此在程序中构建复杂的体系,同时将其复杂性隐在对象的简单性背后。
  4. 每个对象都拥有其类型。照通用的说法,“每个对象都是某个类(class)的一个实例(instance)”。每个类最重要的区别于其他类的特性就是“可以发送什么样的消息给它”。
  5. 某一特定类型的所有对象都可以接收同样的消息。这是一句意味深长的表述。如“圆形”类型的对象同时也是“几何形”类型的对象,所“圆形”对象必定能够接受发送给“几何形”对象的消息。这意味着可以编写与“几何形”交互并自动处理所有与几何形性质相关的事物的代码。这种可替代性是(Object Oriented Programming,OOP)OOP中最强有力的概念之一

如何创建对象

一般我们用new关键字创建一个对象,例如:

这句话的意思大致为“给我一个新的【String】类型的对象,值为【asdf】”

对象的生命周期

  1. 创建阶段(Created)
    1、首先创建对象会查找方法区是否加载了该类的.class文件,没有则java虚拟机加载该类的.class文件,并创建一个类型为Class的对象,这个时候类的所有静态初始化的动作都将执行。因此静态初始化只有在Class对象首次加载时进行一次。
    2、用new关键字创建对象时,java虚拟机将在堆内容上分配内存空间,内存空间会被清空
    4、初始化对象的所有基本类型数据,都设置成默认值而引用类型设置成null。
    一旦对象被创建,并有某个引用指向它,这个对象的状态就切换到了应用阶段

  2. 应用阶段(In Use)
    对象至少被一个强引用持有并且对象在作用域内

  3. 不可见阶段(Invisible)
    程序本身不再持有该对象的任何强引用,但是这些引用可能还存在着。简单说就是程序的执行已经超出了该对象的作用域了。

  4. 不可达阶段(Unreachable)
    该对象不再被任何强引用所持有。

  5. 收集阶段(Collected)
    当垃圾回收器发现该对象已经处于“不可达阶段”并且垃圾回收器已经对该对象的内存空间重新分配做好准备时,则对象进入了“收集阶段”。如果该对象已经重写了finalize()方法,则会去执行该方法。
    尽量不要重写finalize()方法,因为有可能影响JVM的对象分配与回收速度或者可能造成该对象的再次复活

  6. 终结阶段(Finalized)
    当对象执行完finalize()方法之后,仍然处于不可达状态时,则该对象进入终结阶段。在这个阶段,内存空间等待GC进行回收

  7. 对象空间重分配阶段(De-allocated)
    GC对该对象占有的内存空间进行回收或者再分配,该对象彻底消失

继承

什么是继承

类型不仅仅只是描述了作用于一个对象集合上的约束条件,同时还有与其他类型之间的关系。两个类型可以有相同的特性和行为,但是其中一个类型可能比另一个含有更多的特性,并且可以处理更多的消息(或以不同的方式来处理消息)。继承使用基类型和导类型的概念表示了这种类的相似性。一个基类型包含其所有导出类型所共享的特性和行为。可以创建一个基类型来表示系统中某些对象的核心概念,从基类型中导出其他类型,来表示此核心可以被实现的各种不同方式。
在这里插入图片描述
这张UML图中的箭头从导出类(导出类、继承类、基类)指向基类(超类或父类),通常会存在一个以上的导出类

继承语法

使用extends实现类的继承,例如Chinese类继承了Person类:
基类:

public class Instrument {

	public void play(Instrument p){
		System.out.println("Instrument playing");
	}
}

导出类:

public class Wind extends Instrument{

	@Override
	public void play(Instrument  p) {
		System.out.println("Wind Playing");
	}
	
	public static void main(String[] args) {
		Wind c = new Wind();
		c.play(c);
	}
	
}

向上转型

在上面例子中,play()方法可以接受Wind引用。鉴于java对类型的检查十分严格。接受某种类型的方法同样可以接受另外一种类型就会显得很奇怪,除非你认识到c对象同样也是一种Instrument对象,而且也不存在任play()方法是可以通过Instrument调用,同时又不存在Wind 之中,在play()中,程序代码可以对Wind 类和它所有的导出类起作用,这种将导出类引用转换为基类的引用的动作,我们称之为向上转型
在这里插入图片描述
由导出类转型成基类,在继承图上是向上移动因此一般称为向上转型。由于向上转型是从一个较专用类型向较通用类型转换,所以总是很安全的。也就是说,导出类是基类的一个超集。它可能比基类含有更多的方法但它必具备基类中所含有的方法。在向上转型的过程中,类接口中唯一可能发生的事情是丢失方法,而不是获取它们。这就是为什么编译器在“未曾明确表示转型或“未曾指定殊标记”的情况下,任然运行向上转型的原因。

继承与初始化

在对上述例子进行加载的过程中。加载器开始启动并找出 Wind类的编译代码(在名为 Wind. class的文件之中)。在对它进行加载过程中,编译器注意到它有一个基类(这是由关键extends得知的)便会先加载基类,不管你是否打算产生基类的对象,这都要发生 。

如果该基类还有其自身的基类,那么第二个基类就会被加载,如此类推。接下来根基类初始化(在此例中为 Instrument)即会被执行,然后是下一个导出类,以此类推。这种式很重要,因为导出类的static初始化可能会依赖于基类成员能否被正确初始化。

至此为止,必要的类都已加载完毕,对象就可以被创建了。首先,对象中所有的基本类型都会被设为默认值,对象引用被设为null,这是通过将对象内存设为二进制零值而一举生成的。然后,基类的构造器会被调用。基类构造器和导出类的构造器一样,以相同的顺序来经历相同的过程。在基类完成之后,实例变量按其次序被初始化。最构造器的其余部分被执行。

多态

什么是多态

多态是面向对象程序设计的最重要的妙诀:编译器不可能产生传统意义上的函数调用。一个非面向对象编程的编译器产生的函数调用会引起所谓的前期绑定,这么做意味着编译器将产生对一个具体函数名字的调用,而运行时将这个调用解析到将要被执行的代码的绝对地址。然而在OOP中,程序直到运行时才能够确定代码的地址。

为了解决这个问题,面向对象程序设计语言使用了后期绑定(Java中除了static方法和final方法(private属于final方法)之外,其他所有的方法都是后期绑定)的概念。当对象发送消息时,被调用的代码直到运行时才能确定。编译器确保被调用方法的存在,并对调用参数和返回值执行类型检査(无法提供此类保证的语言被称为是弱类型的),但是并不知道将被执行的确切代码

为了执行后期绑定,Java使用一小段特殊的代码来替代绝对地址调用。这段代码使用在对象中存储的信息来计算方法体的地址。这样,根据这一小段代码的内容,每一个对象都可以具有不同的行为表现。

产生正确的行为

向上转型可以像下面这么简单,我们继续使用上面的例子:

这里,创建了一个Wind对象,并把得到的引用立即赋值给i,这样做看似错误(将一种类型赋值给另一种类型);但实际上是没问题的,因为通过继承,Wind就是一种Instrument,因此编译器认可这条语也就不会产生错误信息。

假设现在你调用一个基类方法(它已在导出类中被覆盖)

你可能再次认为调用的是Instrument.play(),因为这毕竟是一个Instrument引用,那么编译器是怎样知做其他的事情呢?由于后期绑定(多态),还是正确调用Wind.play()方法。

协变返回类型

在Java SE5中添加了协变返回类型,它表示在导出类中的被覆盖方法可以返回基类方法的返回类型的某种导出类型。如下例子

class Grain{
	public String toString(){
		return "Grain";
	}
}

class Wheat extends Grain{
	public String toString(){
		return "Wheat";
	}	
}

class Mill{

	public Wheat process(){return new Wheat();}
	
	public static void main(String[] args){
		Grain g = new Grain();
		Mill m = new Mill();
		g = m.process();
		System.out.println(g);
	}
}

输出结果:

Wheat

向下转型与运行时类型识别

通过向下转型—也就是在继承层次中向下移动—应该能够获取类型信息。然而,对于向下转型,例如,我们无法知道一个“几何形状”它确实就是一个“圆形”,它可以是一个三角形、正方形或其他类型。要解决这个问题,必须有某种方法来确保向下转型的正确性。使我们不致于贸然转型到错误类型,进而发出该对象无法接受的消息。这样做是极其不安全的。

Java语言中,所有转型都会得到检査!所以即使我们只是进行一次普通的加括弧形式类型转换,在进入运行期时仍然会对其进行检査,以便保证它的确是我们希望的那种类型。如果不是,就会返ClassCastexception(类转型异常)。这种在运行期间对类型进行检查的行为称作“运行时类型识别(RTTI)”。

final关键字

根据上下文环境,Java的关键字final的含义存在着细微的区别,但通常它指的是“这是无法改变的”。不想做改变可能出于两种理由:设计或效率。由于这两个原因相差很远,所以关键字final有可能被误用 。以下谈论了可能使用到final的三种情况:数据、方法和类。

1.final数据

许多编程语言都有某种方法,来向编译器告知一块数据是恒定不变的。有时数据的恒定不变是很有用的,比如:

  1. 一个永不改变的始译时常量。
  2. 一个在运行时被初始化的值,而你不希望它被改变。

对于编译期常量这种情况,编译器可以将该常量值代入任何可能用到它的计算式中,也就是说,可以在编译时执行计算式,这减轻了一些运行时的负担。在Java中,这类常量必须是基本数据类型,并且以关键字final表示。在对这个常量进行定义的时候,必须对其进行成赋值。

一个既是static又是final的域只占据一段不能改变的存储空间。

当对对象引用而不是基本类型运用final时,其含义会有一点令人迷感。对于基本类型,final使数值恒定不变,而用于对象引用,final使引用恒定不变。一旦引用被初始化指向一个对象,就无法再把它改为指向另一个对象。然而,对象其自身却是可以被修改的,Java并未提供使任何对象恒定不变的途径(但可以自己编写类以取得使对象恒定不变的效果)。这一限制同样适用数组,它也是对象。

public class Test {
	
	final static int I = 1;
	final static double D = Math.random();
		
	public static void main(String[] args) {
		System.out.print("i="+I+",d="+D);
		I = 2;// Error :The final field cannot be assigned
		final Test t = new Test();
		t = new Test();//Error:cannot be assigned
	}
	//输出:i=1,d=0.6484902472577626
}

请注意,带有恒定初始值(即编译期常量)的final static基本类型全用大写字母命名,并且字与字之同用下划线隔开(这就像C常量一样,C常量是这一命名传统的发源地)。我们不能因为某数据是final的就认为在编译时可以知道它的值。示例部分在运行时使用随机生成的数值来初始化的D就说明了这一点。只有当数值在运行时内被初始化时才会显现,因为编译器对编译时数值,一视同仁(并且它们可能因优化而消失)。当运行程序时就会看到这个区别。

空白final

Java允许生成“空白final”,所调空白final是指被声明为final但又未给定初值的域。无论什么情况,编译器都确保空白final在使用前必须被初始化。但是,空白final在关键字final的使用上提供了更大的灵活性,为此,一个类中的final域就可以做到根据对象而有所不同,却又保持其恒定不变的特性。下面即为一例:

class Poppet{
	public final int I;
	public Poppet(int i){
		I = i;
	}
}

public class Test{
	public static void main(String[] args){
		new Poppet(1);
		new Poppet(2);
	}
}

必须在域的定义处或者每个构造器中用表达式对final进行赋值,这正是final域在使用前总是被初始化的原因所在。

final参数

Java允许在参数列表中以声明的方式将参数指明为final。这意味着你无法在方法中更改参数引用所指向的对象

class Test{
	public void f1(final Object o){
		o =  new Object();//Error:variable o cannot be assigned.
	}

	public void f2(final int i){
		i++;//variable i cannot be assigned.
	}
}

2.final方法

使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义。这是出于设计的考虑:想要确保在继承中使方法行为保持不变,并且不会被覆盖。

过去建议使用final方法的第二个原因是效率。在Java的早期实现中,如果将一个方法指明为final,就是同意编译器将针对该方法的所有调用都转为内嵌调用。当编译器发现一个final方法调用命令时,它会根据自己的谨慎判断,跳过插入程序代码这种正常方式而执行方法调用机制(将参数压入栈,跳至方法代码处并执行,然后跳回并清理栈中的参数,处理返回值),并且以方法体中的实际代码的副本来替代方法调用。这将消除方法调用的开销。当然,如果一个方法很大,你的程序代码就会膨胀,因而可能看不到内嵌带来的任何性能提高,因为,所带来的性能提高会因为花费于方法内的时间量而被缩减。

在最近的Java版本中,虚拟机(特別是hotspot技术)可以探测到这些情况,并优化去掉这些效率反而降低的额外的内嵌调用,因此不再需要使用final方法来进行优化了。事实上,这种做法正在逐渐地受到劝阻。在使用JavaSE5/6时,应该让编译器和JVM去处理效率问题,只有在想
要明确禁止覆盖时,才将方法设置为final的 。

final和private关键字

类中所有的private方法都隐式地指定为是final的。由于无法取用private方法,所以也就无法覆盖它。可以对private方法添加final修饰词,但这并不能给该方法增加任何额外的意义。

这一问题会造成混淆。因为,如果你试图覆盖一个private方法(隐含是final的),似乎是奏效的,而且编译器也不会给出错误信息:

class WithFinals{
	private final void f(){
		System.out.print("WithFinals.f()");
	}
}

public class OverridingPrivate extends WithFinals{
	private final void f(){
		System.out.print("OverridingPrivate.f()");
	}
	
	public void g(){
		f();
	}
	public static void main(String[] args){
		OverridingPrivate o = new OverridingPrivate();
		o.g();
	}
	//输出:OverridingPrivate.f()
}

“覆盖”只有在某方法是基类的接口的一部分时才会出现。即必须能将一个对象向上转型为它的基本类型并调用相同的方法。如果某方法为private,它就不是基类的接口的一部分。它仅是一些隐藏于类中的程序代码,只不过是具有相同的名称而已。但如果在导出类中以相同的名称生成一个public、protected或包访问权限方法的话,该方法就不会产生在基类中出现的“仅具有相同名称”的情况。此时你并没有覆盖该方法,仅是生成了一个新的方法。由于private方法无法触及而且能有效隐藏,所以除了把它看成是因为它所归属的类的组织结构的原因而存在外,其他任何事物都不需要考虑到它。

3.final类

当将某个类的整体定文为final时(通过将关键字final置于它的定文之前),就表明了你不打算继承该类,而且也不允许别人这样做。换句话说,出于某种考虑,你对该类的设计永不需要做任何变动,或者出于安全的考虑,你不希望它有子类。例:

final class Diosaur{}

class Further extends Diosaur{} //Error:Cannot extend final class 'Diosaur'

请注意,final类的域可以根据个人的意愿选择为是或不是final。不论类是否被定义为final,相同的规则都适用于定文为final的域。然而,由于final类禁止继承,所以final类中所有的方法都隐式指定为是final的,因为无法覆盖它们。在final类中可以给方法添加final修饰词,但这不会增添任何意义 。

有关final的忠告

在设计类时,将方法指明是final的,应该说是明智的。你可能会觉得,没人会想要覆差你的方法。有时这是对的。但请留意你所作的假设。要预见类是如何被复用的一般是很困难的,特别是对于一个通用类而言更是如此。如果将一个方法指定为final,可能会妨碍其他程序员在项目中通过继承来复用你的类,而这只是因为你没有想到它会以那种方式被运用 。


  1. 本文来源《Java编程思想(第四版)》
  2. 对象生命周期原文:https://blog.csdn.net/sodino/article/details/38387049