Thinking in Java 4 : 一切都是对象

简介: 在Java的世界里,任何东西都可看作对象,但操纵的标识符实际是指向一个对象的“句柄”(Handle)或称作一个“引用”。

“尽管以C++为基础,但Java是一种更纯粹的面向对象程序设计语言”。

1.用句柄操纵对象

在Java的世界里,任何东西都可看作对象,但操纵的标识符实际是指向一个对象的“句柄”(Handle)或称作一个“引用”。

为了便于理解,可将这一情形想象成用遥控板(句柄)操纵电视机(对象),只要握住这个遥控板,就相当于掌握了与电视机连接的通道;一旦需要“换频道”或者“调声音”,实际操纵的是遥控板(句柄),再由遥控板去操纵电视机(对象);如果要在房间里四处走走,并想保持对电视机的控制,那么手上拿着的是遥控板,而非电视机。

此外,即使没有电视机,遥控板亦可独立存在。也就是说,拥有一个句柄,并不表示必须要有一个对象同它连接。

String s;

上述创建的只是句柄,并不是对象,若此时向s发送一条消息,就会获得一个错误(运行期),这是由于s实际并未与任何东西连接(即“没有电视机”)。

因此,一种更安全的做法是:创建一个句柄时,记住无论如何都要进行初始化:

Strings="asdf";

2.所有对象都必须创建

创建句柄时,我们希望它同一个新对象连接,通常用new关键字达到这一目的,所以在上面的例子中,可以用:

Strings=newString("asdf");

它不仅指出“将我变成一个新字串”,也通过提供一个初始字串,指出了“如何生成这个新字串”。

3.数据保存到什么地方?

程序运行时,最好对数据保存到什么地方做到心中有数,特别要注意的是内存的分配,有六个地方都可以保存数据:

(1) 寄存器

这是最快的保存区域,位于处理器(CPU)内部,但是,寄存器的数量十分有限,它是根据需要由编译器分配,我们对此没有直接的控制权,也不可能在自己的程序里找到寄存器存在的任何踪迹。

(2) 堆栈

驻留于常规RAM(随机访问存储器)区域,但可通过它的“堆栈指针”获得处理的直接支持。这是一种特别快、特别有效的数据保存方式,仅次于寄存器。创建程序时,Java编译器必须准确地知道堆栈内保存的所有数据的“长度”以及“存在时间”,这一限制无疑影响了程序的灵活性,所以尽管有些Java数据要保存在堆栈里,特别是对象句柄,但Java对象并不放到其中。

(3) 堆

一种常规用途的内存池(也在RAM区域),其中保存了Java对象。和堆栈不同,“内存堆”或“堆”(Heap)最吸引人的地方在于编译器不必知道要从堆里分配多少存储空间,也不必知道存储的数据要在堆里停留多长的时间,因此,用堆保存数据时会得到更大的灵活性。

要求创建一个对象时,只需用new命令编制相关的代码即可,执行这些代码时,会在堆里自动进行数据的保存;当然,为达到这种灵活性,必然会付出一定的代价:在堆里分配存储空间时会花掉更长的时间!

(4) 静态存储

这儿的“静态”(Static)是指“位于固定位置”(尽管也在RAM里),程序运行期间,静态存储的数据将随时等候调用,可用static关键字指出一个对象的特定元素是静态的,但Java对象本身永远都不会置入静态存储空间。

(5) 常数存储

常数值通常直接置于程序代码内部,这样做是安全的,因为它们永远都不会改变。有的常数需要严格地保护,所以可考虑将它们置入只读存储器(ROM)。

(6) 非RAM存储

若数据完全独立于一个程序之外,则程序不运行时仍可存在,并在程序的控制范围之外。其中两个最主要的例子便是“流式对象”和“固定对象”,对于流式对象,对象会变成字节流,通常会发给另一台机器;而对于固定对象,对象保存在磁盘中,即使程序中止运行,它们仍可保持自己的状态不变。

对于这些类型的数据存储,一个特别有用的技巧就是它们能存在于其他媒体中,一旦需要,甚至能将它们恢复成普通的、基于RAM的对象。

4.特殊情况:主要类型

有一系列类需特别对待,之所以要特别对待,是由于用 new 创建对象(特别是小的、简单的变量)并不是非常有效,因为 new 将对象置于“堆”里。

对于这些类型, Java 采纳了与 C 和 C++相同的方法,也就是说,不是用new 创建变量,而是创建一个并非句柄的“自动”变量,这个变量容纳了具体的值,并置于堆栈中,能够更高效地存取。

Java 决定了每种主要类型的大小,就象在大多数语言里那样,这些大小并不随着机器结构的变化而变化,这种大小的不可更改正是 Java 程序具有很强移植能力的原因之一。

主类型 大小 最小值 最大值 封装器类型
boolean 1 位 - - Boolean
char 16 位 Unicode 0 Unicode 2 的 16 次方-1 Character
byte 8 位 -128 +127 Byte
short 16 位 -2 的 15 次方 +2 的 15 次方-1 Short
int 32 位 -2 的 31 次方 +2 的 31 次方-1 Integer
long 64 位 -2 的 63 次方 +2 的 63 次方-1 Long
float 32 位 IEEE754 IEEE754 Float
double 64 位 IEEE754 IEEE754 Double
void - - - Void

Java 增加了两个类,用于进行高精度的计算: BigInteger 和 BigDecimal。

这两个类都有自己特殊的“方法”,对应于我们针对主类型执行的操作,也就是说,能对int 或 float 做的事情,对 BigInteger 和 BigDecimal 一样可以做,只是必须使用方法调用,不能使用运算符。此外,由于牵涉更多,所以运算速度会慢一些。我们牺牲了速度,但换来了精度。

BigInteger 支持任意精度的整数。也就是说,我们可精确表示任意大小的整数值,同时在运算过程中不会丢失任何信息;

BigDecimal 支持任意精度的定点数字。例如,可用它进行精确的币值计算。

5.作用域

大多数程序设计语言都提供了“作用域”( Scope)的概念。对于在作用域里定义的名字,作用域同时决定了它的“可见性”以及“存在时间”。在 C, C++和 Java 里, 作用域是由花括号的位置决定的。

作为在作用域里定义的一个变量,它只有在那个作用域结束之前才可使用。

对象的作用域

Java 对象不具备与主类型一样的存在时间。用 new 关键字创建一个 Java 对象的时候,它会超出作用域的范围之外。所以假若使用下面这段代码:

{
String s = new String("a string");
} /* 作用域的终点 */

那么句柄 s 会在作用域的终点处消失。然而, s 指向的 String 对象依然占据着内存空间。在上面这段代码里,我们没有办法访问对象,因为指向它的唯一一个句柄已超出了作用域的边界。

这样造成的结果便是:对于用 new 创建的对象,只要我们愿意,它们就会一直保留下去。这个编程问题在C和 C++里特别突出,在C++里,一旦工作完成,必须保证将对象清除。

这样便带来了一个有趣的问题?假如 Java 让对象依然故我,怎样才能防止它们大量充斥内存,并最终造成程序的“凝固”呢。在 C++里,这个问题最令程序员头痛。但 Java 以后,情况却发生了改观。

Java 有一个特别的“垃圾收集器” ,它会查找用 new 创建的所有对象,并辨别其中哪些不再被引用。随后,它会自动释放由那些闲置对象占据的内存,以便能由新对象使用,这意味着我们根本不必操心内存的回收问题,只需简单地创建对象,一旦不再需要它们,它们就会自动离去。

这样做可防止在 C++里很常见的一个编程问题:由于程序员忘记释放内存造成的“内存溢出”。

6.新建数据类型:类

如果说一切东西都是对象,那么用什么决定一个“类”( Class)的外观与行为呢?换句话说,是什么建立起了一个对象的“类型”( Type)呢?大多数面向对象的语言都用关键字“ class”表达这样一个意思。

定义一个类时,可在类里设置两种类型的元素:数据成员(也叫“字段”)以及成员函数(也叫“方法”)。

每个对象都为自己的数据成员保有存储空间;数据成员不会在对象之间共享。下面是定义了一些数据成员的类示例:

class DataOnly {
int i;
float f;
boolean b;
}

这个类并没有做任何实质性的事情,但我们可创建一个对象:

DataOnly d = new DataOnly();

可将值赋给数据成员,但首先必须知道如何引用一个对象的成员。为达到引用对象成员的目的,首先要写上对象句柄的名字,再跟随一个点号(句点),再跟随对象内部成员的名字。即“ 对象句柄.成员”。例如:

d.i = 47;
d.f = 1.1f;
d.b = false;

一个对象也可能包含了另一个对象,而另一个对象里则包含了我们想修改的数据。对于这个问题,只需保持“连接句点”即可。例如:

myPlane.leftTank.capacity = 100;

主成员的默认值

若某个主数据类型属于一个类成员,那么即使不明确(显式)进行初始化,也可以保证它们获得一个默认值。

主类型 默认值
Boolean false
Char 'u0000'(null)
byte (byte)0
short (short)0
int 0
long 0L
float 0.0f
double 0.0d

然而,这种保证却并不适用于“局部”变量—— 那些变量并非一个类的字段。

7.方法、自变量和返回值

迄今为止,我们一直用“函数”( Function)这个词指代一个已命名的子例程,但在 Java 里,更常用的一个词却是“方法”( Method),代表“完成某事的途径”。

Java 的“方法”决定了一个对象能够接收的消息,方法的基本组成部分包括名字、自变量、返回类型以及主体。下面便是它最基本的形式:

返回类型 方法名( / 自变量列表/ ) {/ 方法主体 /}

返回类型是指调用方法之后返回的数值类型。显然,方法名的作用是对具体的方法进行标识和引用。

自变量列表列出了想传递给方法的信息类型和名称。Java 的方法只能作为类的一部分创建。

为一个对象调用方法时,需要先列出对象的名字,在后面跟上一个句点,再跟上方法名以及它的参数列表。

对象名.方法名(自变量 1,自变量 2,自变量 3...)

面向对象的程序设计通常简单地归纳为“向对象发送消息”。“静态”方法可针对类调用,毋需一个对象。

自变量列表

自变量列表规定了我们传送给方法的是什么信息,在传递对象时,通常都是指传递指向对象的句柄。

对于前面提及的“特殊” 数据类型 boolean,char,byte, short, int,long, float 以及 double 来说是一个例外。

8.构建 J a v a 程序

正式构建自己的第一个 Java 程序前,还有几个问题需要注意。

1.名字的可见性

在所有程序设计语言里,一个不可避免的问题是对名字或名称的控制,假设您在程序的某个模块里使用了一个名字,而另一名程序员在另一个模块里使用了相同的名字,此时,如何区分两个名字,并防止两个名字互相冲突呢?这个问题在 C 语言里特别突出,因为程序未提供很好的名字管理方法。

为解决这个问题, C++用额外的关键字引入了“命名空间”的概念。

由于采用全新的机制,所以 Java 能完全避免这些问题,为了给一个库生成明确的名字,采用了与Internet域名类似的名字。

事实上, Java 的设计者鼓励程序员反转使用自己的 Internet 域名,因为它们肯定是独一无二的,比如我的域名是 alisoft.com,所以我的实用工具库就可命名为com. alisoft.utility,整个软件包都以小写字母为标准。

Java 的这种特殊机制意味着所有文件都自动存在于自己的命名空间里,而且一个文件里的每个类都自动获得一个独一无二的标识符(当然,一个文件里的类名必须是唯一的)。

2.使用其他组件

一旦要在自己的程序里使用一个预先定义好的类,编译器就必须知道如何找到它,当然,这个类可能就在发出调用的那个相同的源码文件里,但假若那个类位于其他文件里呢?为达到这个目的,要用import 关键字准确告诉Java 编译器我们希望的类是什么。

import 的作用是指示编译器导入一个“包”,或者说一个“类库”(在其他语言里,可将“库”想象成一系列函数、数据以及类的集合。但请记住, Java 的所有代码都必须写入一个类中)。

3. s t a t i c 关键字

通常,我们创建类时会指出那个类的对象的外观与行为,除非用new 创建那个类的一个对象,否则实际上并未得到任何东西,只有执行了 new 后,才会正式生成数据存储空间,并可使用相应的方法。

但在两种特殊的情形下,上述方法并不堪用,一种情形是只想用一个存储区域来保存一个特定的数据,无论要创建多少个对象,甚至根本不创建对象;另一种情形是我们需要一个特殊的方法,它没有与这个类的任何对象关联,也就是说,即使没有创建对象,也需要一个能调用的方法。

为满足这两方面的要求,可使用static(静态)关键字。一旦将什么东西设为 static,数据或方法就不会同那个类的任何对象实例联系到一起。

对于非 static 数据和方法,我们必须创建一个对象,并用那个对象访问数据或方法,这是由于非static 数据和方法必须知道它们操作的具体对象,当然,在正式使用前, 由于 static 方法不需要创建任何对象,所以它们不可简单地调用其他那些成员,同时不引用一个已命名的对象,从而直接访问非 static 成员或方法(因为非 static 成员和方法必须同一个特定的对象关联到一起)。

有些面向对象的语言使用了“类数据”和“类方法”这两个术语,它们意味着数据和方法只是为作为一个整体的类而存在的,并不是为那个类的任何特定对象。有时,您会在其他一些Java 书刊里发现这样的称呼。

为了将数据成员或方法设为 static,只需在定义前置和这个关键字即可。例如,下述代码能生成一个 static数据成员,并对其初始化:

class StaticTest {
   static int i = 47;
}

现在,尽管我们制作了两个 StaticTest 对象,但它们仍然只占据 StaticTest.i 的一个存储空间。这两个对象都共享同样的 i。请考察下述代码:

StaticTest st1 = new StaticTest();
StaticTest st2 = new StaticTest();

此时,无论 st1.i 还是 st2.i 都有同样的值 47,因为它们引用的是同样的内存区域。

有两个办法可引用一个 static 变量,正如上面展示的那样,可通过一个对象命名它,如 st2.i。亦可直接用它的类名引用,而这在非静态成员里是行不通的(最好用这个办法引用static 变量,因为它强调了那个变量的“静态”本质)。

StaticTest.i++;

其中, ++运算符会使变量增值。此时,无论 st1.i 还是 st2.i 的值都是 48。
类似的逻辑也适用于静态方法。既可象对其他任何方法那样通过一个对象引用静态方法,亦可用特殊的语法格式“类名.方法()” 加以引用。

9.第一个 J a v a 程序

最后,让我们正式编一个程序。它能打印出与当前运行的系统有关的资料,并利用了来自Java 标准库的 System 对象的多种方法。

import java.util.Date;
import java.util.Properties;

public class Property {
    public static void main(String[] args) {
        System.out.println(new Date());
        Properties p = System.getProperties();
        p.list(System.out);
        System.out.println("--- Memory Usage:");
        Runtime rt = Runtime.getRuntime();
        System.out.println("Total Memory = " + rt.totalMemory() 
                        + " Free Memory = " + rt.freeMemory());
    }
}

输出参考:

Wed Dec 20 15:51:41 CST 2017
-- listing properties --
java.runtime.name=Java(TM) SE Runtime Environment
sun.boot.library.path=C:Program Files (x86)Javajdk1.6.0_...
java.vm.version=20.45-b01
java.vm.vendor=Sun Microsystems Inc.

java.vendor.url=http://java.sun.com/
path.separator=;
java.vm.name=Java HotSpot(TM) Client VM
file.encoding.pkg=sun.io
user.country=CN
sun.java.launcher=SUN_STANDARD
sun.os.patch.level=Service Pack 3
java.vm.specification.name=Java Virtual Machine Specification
user.dir=F:MavenSpaceerp-parenterp-web
java.runtime.version=1.6.0_45-b06
java.awt.graphicsenv=sun.awt.Win32GraphicsEnvironment
java.endorsed.dirs=C:Program Files (x86)Javajdk1.6.0_...
os.arch=x86
java.io.tmpdir=C:UsersADMINI~1AppDataLocalTemp\
line.separator=

java.vm.specification.vendor=Sun Microsystems Inc.
user.variant=
os.name=Windows XP
sun.jnu.encoding=GBK
java.library.path=C:Program Files (x86)Javajdk1.6.0_...
java.specification.name=Java Platform API Specification
java.class.version=50.0
sun.management.compiler=HotSpot Client Compiler
os.version=5.1
user.home=C:UsersAdministrator
user.timezone=Asia/Shanghai
java.awt.printerjob=sun.awt.windows.WPrinterJob
file.encoding=UTF-8
java.specification.version=1.6
user.name=Administrator
java.class.path=F:MavenSpaceerp-parenterp-webtarge...
java.vm.specification.version=1.0
sun.arch.data.model=32
java.home=C:Program Files (x86)Javajdk1.6.0_...
sun.java.command=com.jsjn.zxpt.DefaultTest
java.specification.vendor=Sun Microsystems Inc.
user.language=zh
awt.toolkit=sun.awt.windows.WToolkit
java.vm.info=mixed mode, sharing
java.version=1.6.0_45
java.ext.dirs=C:Program Files (x86)Javajdk1.6.0_...
sun.boot.class.path=C:Program Files (x86)Javajdk1.6.0_...
java.vendor=Sun Microsystems Inc.
file.separator=\
java.vendor.url.bug=http://java.sun.com/cgi-bin/bugreport...

sun.cpu.endian=little
sun.io.unicode.encoding=UnicodeLittle
sun.desktop=windows
sun.cpu.isalist=pentium_pro+mmx pentium_pro pentium+m...
--- Memory Usage:
Total Memory = 16252928 Free Memory = 15446800

相关文章
C4.
|
1月前
|
缓存 Java
Java的Integer对象
Java的Integer对象
C4.
11 0
|
19天前
|
Java
java8中List对象转另一个List对象
java8中List对象转另一个List对象
36 0
|
6天前
|
存储 Java 编译器
对象的交响曲:深入理解Java面向对象的绝妙之处
对象的交响曲:深入理解Java面向对象的绝妙之处
35 0
对象的交响曲:深入理解Java面向对象的绝妙之处
|
11天前
|
Java
在Java中,多态性允许不同类的对象对同一消息做出响应
【4月更文挑战第7天】在Java中,多态性允许不同类的对象对同一消息做出响应
16 2
|
19天前
|
Java
Java常用封装Base对象
Java常用封装Base对象
8 0
|
27天前
|
Java
【Java】通过Comparator比较器的方式给对象数组排序
【Java】通过Comparator比较器的方式给对象数组排序
8 0
|
27天前
|
Java
【Java】重写compareTo()方法给对象数组排序
【Java】重写compareTo()方法给对象数组排序
10 0
|
1月前
|
Java 数据安全/隐私保护
JAVA对象
JAVA对象
10 0
|
1月前
|
存储 Java
Java-对象
Java-对象
15 0
|
1月前
|
存储 Java 对象存储
Java对象和类
Java对象和类
14 1