01 对象无所不在
虽然源于C++,Java却是一门更为“纯正”的面向对象语言。
Java和C++都是混合型语言,混合型编程语言允许采用多种编程风格。
Java语言预期只编写面向对象程序。在本章,你将学习构建Java程序的基础组件,并认识Java中(几乎)一切都是对象这个事实。
1、通过引用操作对象
- 实际操作的是引用,再由引用来修改对象信息
- 引用可以不关联对象独立存在
所有编程语言都在内存中处理各种元素。
String s;
这样只是创建了一个引用而非对象。此时向对象s发送消息,则会得到一条报错信息NullPointException,因为此时s还没有连接到任何对象。
一种安全做法:始终在创建对象时进行初始化。
String s = "asdf";
此处使用了Java的一个特性:字符串(String)可以用带引号的文本进行初始化。而对于其它类型的对象而言,需要使用 new 进行初始化。(更为通用)
2、必须创建所有对象
- 引用的作用是关联对象
- 通常我们使用new关键字来创建对象
new关键字表达的意思是“我要一个这种类型的对象”。
String s = new String("asdf");
这一行代码不仅代表了“创建一个新的字符串”,同时也告诉我们如何如何通过一组字符来创建String对象。
2.1 数据保存在哪里
- 寄存器(register):这是速度最快的数据存储方式,因为它保存数据的位置不同与其它方式:数据会直接保存在*中央处理器(CPU)*里。然而寄存器的数量有限,所以只能按需分配。此外,我们不能直接控制寄存器的分配,甚至在程序中都找不到寄存器存在过的证据(C 和 C++除外)
- 栈(stack):数据存储在随机存储器(random-access memory,RAM)里,处理器可以通过栈指针(stack pointer)直接操作该数据。具体来说,栈指针向下移动将申请一块新的内存,向上移动则会释放这块内存。这是一种极其迅速和高效的内存分配方式,其效率仅次于寄存器。只不过Java系统在创建应用程序时就必须明确栈上所有对象的生命周期。这种限制约束了程序的灵活性,因此虽然有一些数据会保存在栈上(尤其是对象引用),对象本身并非如此。
- 堆(heap): 这是一个通用的内存池(使用的也是RAM空间),用于存放所有Java对象。与栈不同,编译器并不关心堆上的对象需要存在多久。因此,堆的使用是非常灵活的。当你需要一个对象时,可以随时在堆上为该对象分配内存空间。然而这种灵活性是有代价的:分配和清理堆存储要比栈存储花费更多的时间。好消息是:随着时间的推移,Java的堆内存分配机制已经变得非常高效了,所以你并不需要太过关注此类问题。
- 常量存储(constant storage):常量通常会直接保存在程序代码中(方法区)中,因为它们的值不会改变,所以这样做是安全的。有时候常量会与其它代码隔离开来,于是在某些嵌入系统里,这些常量就可以保存在只读存储器(read-only memory,ROM)中。
- 非RAM存储(non-RAM storage): 如果一段数据没有保存在应用程序里,那么该数据的生命周期既不依赖于应用程序是否运行,也不受应用程序的管制。其中最典型的例子之一是“序列化对象”(serialized object),它指的是转换为字节流(叫作“序列化”)并可以发送至其他机器的对象,而这些对象即便在程序结束运行之后也依然能够保持其状态。这些数据存储类型的特点在于,它们会将对象转换成其他形式以保存于其他媒介中,然后在需要的时候重新转换为常规的RAM对象。Java支持轻量级的持久化对象存储,而JDBC以及Hibernate等库则提供了更为成熟的解决方案,即支持使用数据库存取对象信息。
2.2 特殊情况:基本类型
有些经常使用的类型享受特殊待遇,可以将它们称为“基本类型”(primitive type)。其之所以可以享受特殊待遇,是因为new关键字是在堆上创建对象 ,这就意味着哪怕是创建一些简单的便利也不会很高效。对于基本类型,Java采用了 C 以及 C++ 相同的实现机制,这意味着我们无需使用 new 来创建基本类型的变量,而是直接创建一个“自动变量”(automatic variable),注意不是引用。也就是说,该变量会直接在栈上保存它的值,因此运行效率也比较高。
Java每一种基本类型在不同机器上所占用的空间也是保持一致的。这种一致性也就是Java程序移植性更好的原因。
基本类型 | 大小 | 最小值 | 最大值 | 包装类 |
---|---|---|---|---|
boolean | - | - | - | Boolean |
char | 16位 | Unicode\u0000 | Unicode\uffff | Character |
byte | 8位 | -2^7 | 2^7 - 1 | 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 |
以上所有数值类型都是有符号的。
boolean的空间大小没有明确标出,其对象只能被赋值 true 或 false。
Java为基本数据类型提供了对应的“包装类”(wrapper class),通过包装类可以将基本类型呈现为位于堆上的非原始对象。例如:
-
char c = 'x'; Character ch = new Character(c);
-
Character ch = new Character('x');
“自动装箱”机制(autoboxing)能够将基本类型对象自动转换为包装类对象,例如:
-
Character ch = 'x';
-
也可以在转换回来:自动拆箱
char c = ch;
2.3 高精度数字
Java中提供了两个支持高精度计算的类:精度换速度
- BigInteger:支持任意精度的整数,在运算时可以表示任意精度的整数值,而不用担心丢失精度。
- BigDecimal:可用于任意精度的定点数。例如,可以将其用于货币计算。
详细看附录2:02.附录:Java中Math、BigInteger、BigDecimal详解
2.4 Java数组
Java的一个核心设计目的是安全,于是Java解决了C和C++使用数组的许多问题:
- Java的数组一定会被初始化
- 无法访问Java数组边界之外的元素
这种边界检查的代价是需要消耗少许内存,以及运行时需要少量时间来验证索引的正确性。其背后的假设是:安全性和生产力的改善完全可以抵消这些代价,Java同时也会优化这些操作。
- 创建一个对象数组,数组里存放的实际上是对象引用,这些引用会被自动初始化为特殊值 null。Java会认为一个值为null的引用没有指向任何对象,操作引用前需要确保其指向了某个对象。否则 NullPointException。
- 创建一个基本类型数组,编译器会确保该数据进行初始化,并将数组元素值设为 0。
3、注释
单行注释://
多行注释:/**/
4、无须销毁对象
数据的生命周期所造成的困扰会导致大量 bug。本节将展示 Java 如何释放内存。
4.1 作用域
作用域(scope)会决定其范围内定义的变量名的 可见性 和 生命周期。C、C++、Java的作用域范围都是通过 {} 来定义的,下面是一个 Java 作用域的例子:
{
int x = 12;
// 只有变量 x 可见
{
int q = 96;
// 变量 x 和 变量 q 都可见
}
// 只有变量 x 可见
// 超出了变量 q 的作用域
}
在作用域内定义的变量只在该作用域的范围内可见。
代码缩进可以提高Java代码的可读性。
在 C、C++ 中下面代码是合法的,但是Java不能这么用:Java编译器会报错:变量 x 已定义
{
int x = 12;
{
int x = 96; // 语法错误
}
}
4.2 对象的作用域
Java对象的生命周期和基本类型有所不同。当使用 new 创建一个对象,该对象在作用域结束后会依然存在。
{
String s = new String("a hhahh");
} // 作用域结束
虽然引用 s 会在作用域结束后会消失,但是它指向的String对象还会继续占用内存。
对于上面代码而言,该String对象在作用域结束后就无法再获取,因为已经超出了其唯一引用的作用域范围。如何在程序中传递和复制对象的引用?
如果需要的话,通过new创建的对象会存在足够长的时间,因为Java无须像C++一样,不仅要确保对象在需要的时候随时可用,而且事后还需要负责销毁这些对象。
Java的垃圾收集器会监视所有通过 new 创建的对象,并及时发现哪些对象不再被引用,然后它会释放这些对象所占用的内存,使得这些内存可以用于其它新对象。这种机制解决了一类非常严重的编程问题:由于程序员忘记释放内存而导致“内存泄漏”(memory leak)。
5、使用 class 关键字创建新类型
大多数面向对象编程语言会使用“class”关键字来描述新的对象种类。其用法就是 class 关键字后面跟着新的类名:
class ATypeName {
// 类的具体实现放在这里
}
上面代码创建了一个新的类,只不过其内容只包含一行注释,一次目前也没有什么实际的作用。话虽如此,你还是可以通过 new 关键字创建一个该类的对象。
ATypeName a = new ATypeName();
5.1 字段
定义一个类,你可以为其定义两种元素:字段 和 方法。我们通过对象的引用与字段进行交互。字段可以是任何类型的对象,也可以是基本类型。如果一个字段是某个对象的引用,则必须通过 new 关键字初始化该引用并将其关联到具体的对象上。
每一个对象都会单独保存其字段,通常来说,不同对象的字段之间不会共享。
class DataOnly {
int i;
double d;
boolean b;
}
可以通过对象成员为字段赋值。具体的做法是,先写出对象的引用名,跟着再写一个 “.” ,然后是对象的成员名:objectReference.member
(通过对象引用和字段进行交互)
例如:
DataOnly data = new DataOnly();
data.i = 47;
data.d = 1.1;
data.b = false;
如果一个对象包含了其它对象,而想修改其它对象的数据,同样可以使用 “.” 来实现:myPlane.leftTank.capacity = 100;
理论上可以使用这种方法嵌套无穷多的对象,但是不够优雅。
5.2 基本类型的默认值
当一个类的字段为基本类型时,即使没有对其进行初始化,它们也会拥有默认值:
基本类型 | 默认值 |
---|---|
char | \u0000(null) |
boolean | false |
byte | 0 |
short | 0 |
int | 0 |
long | 0L |
float | 0.0f |
double | 0.0d |
当变量作为类成员存在时,Java才会将其初始化为以上的默认值。这一点确保了基本类型的字段一定会被初始化。
只不过对于我们编写的代码而言,这些默认值可能并不是正确或合理的值,所以最佳的实践应该是显示初始化这些变量。
此外,这种机制并不会应用于局部变量(local variable),因为局部变量并不是类的字段。
Java中,一个局部变量如果未进行初始化就使用,Java会抛出一个编译错误以告知你变量没有被初始化。
6、方法、参数及返回值
在Java中,方法/函数(method/function)是“做某件事的方式/行为”。
Java中的方法决定了对象可以接受哪些消息。方法最基础的几个部分包括:方法名、返回值、参数,以及方法体。例如:
ReturnType methodName(/* 参数列表 */) {
// 方法体
}
ReturnType 表示当调用一个方法时,该方法生成的值是什么类型。参数列表提供了一系列需要传递给方法的信息,包括信息的类型和名称。方法名和参数列表共同构成了方法的“签名”,方法签名即该方法的唯一标识符。
Java中的方法只能作为类的一部分存在,方法只能通过对象调用,而该对象必须能执行该方法。如果通过对象调用一个不属于该对象的方法,编译报错。
调用方法的具体方式:对象.方法名及其参数列表:objectReference(arg1, arg2, arg3)
如果定义一个无参且返回int值的f()方法。假设有对象 a 定义了 f() 方法:int a = f();
注意: 返回值类型必须与接收变量的类型保持一致。
面向对象编程:向对象发送消息
6.1 参数列表
参数列表描述了需要传递给方法的信息。这些信息和Java中其它内容一样都是对象。所以,参数列表也需要描述这些对象的类型和名称。和之前一样,操作对象时,实际操作的是它的引用。此外,引用类型必须正确,如果方法定义的参数是 String 类型,就必须传递一个 String 对象给该方法,否则编译报错。
方法必须定义在一个类的内部。
return 关键字的作用:
- 告诉我们“从方法中离开吧,一切都结束了”。
- 当该方法生成了一个值的时候,将这个值放置于 return 之后。
方法可以返回任意类型的值,但是如果不返回任何值,就表示该方法生成的值为 void。
当返回值的类型是 void 时,使用 return 关键字的作用是退出该方法,因此在方法的末尾处就没有必要使用 return 方法了。此外,可以在方法的任意位置使用 return。然而,一旦返回值是非 void 类型,无论在何处返回,编译器都会强制要求必须提供正确类型的返回值。
如此看来,Java 程序似乎就是一群对象将其他对象设置为其方法的参数,然后向这些对象发送消息。
7、编写Java程序
7.1 名称可见性
所有的编程语言都有一个问题:对名称的控制——即防止名称冲突。
Java 为了能够清晰的描述库的名称,Java语言的设计者所使用的方法是将你的互联网域名反转使用。因为域名是唯一的,所以一定不会冲突。反转之后的域名之后的几个 “.” ,实际上描述了子目录的结构。
这种机制确保了所有文件都有对应的命名空间,同时文件里定义的类都具有唯一对应的标识符。因此,Java语言就避免了命名冲突的问题。
7.2 使用其它组件
当你在程序中使用预定义的类时,编译器必须找到这个类。我们需要消除所有类似类名冲突这样的潜在歧义,解决方案则是利用 import 关键字告知 Java 编译器你想使用哪个类。import 语句的作用是通知编译器导入一个指定位置的包,即放置各种类的库。
开发中经常会用到编译器自带的各种 Java 标准库组件。当你使用这些组件时,无须担心哪些冗长和反转的域名,使用下面的方式即可导入:
import java.util.ArrayList;
这一行代码会告诉编译器,我们要使用位于 util 库的 ArrayList 类。
此外。util 库也包括了一些其它的类,可能你需要使用其中的几个类,却不想一个个地导入它们。这种情况下,可以使用通配符 “*” 来轻松实现这一点:
import java.util.*;
注意:许多编程风格指南明确指出:每一个用到的类都应该被单独导入。
7.3 static 关键字
创建一个类即描述了其对象的外观和行为。直到使用 new 关键字时,才会真正创建一个对象,以及为该对象分配内存,并使得其方法可以被调用。
然而在两种情况下,这种方式会显得不合时宜:
- 有时候我们需要一小块共享空间来保存某个特定的字段,而不关心创建多少个对象,甚至有没有创建对象
- 需要使用一个类的某个方法,而该方法和具体的对象无关;换句话说,希望即便没有生成该类的对象,依然可以调用其方法
static 关键字(源自 C++)为上面两个问题提供了解决方案。
如果使用 static 关键字,则意味着使用 static 的字段或方法不依赖于任何对象。也就是说,即便没有为一个类创建任何对象,依旧可以调用该类的 static 方法或 static 字段。另外,由于非 static 的字段和方法必须基于指定的对象,因此对于非 static 的字段和方法来说,必须创建一个对象才可以使用非 static 的字段和方法。
创建一个 static 字段或方法,只需要把 static 关键字放置于字段或方法定义的前面即可:
class StaticTest {
static int i = 47;
}
即便创建了两个 StaticTest 类的对象,StaticTest.i 依然只会占用同一块内存空间。即,字段 i 会被 StaticTest 的所有实例对象所共享。
有两种方法可以调用 static 变量/方法:
- 通过对象调用
- 通过类名调用(非 static 成员不能这样使用)——推荐使用,突出了变量的 static 特质
将 static 关键字应用于字段,毫无疑问会改变其数据的生成方式,即 static 字段是基于类创建的,非 static 字段则是基于对象创建的。而将 static 应用于方法时,即便没有创建对象,也可以通过类名调用该方法。后面会看到,作为程序应用的入口,main 方法中 static 关键字是必不可少的一部分。
8、你的第一个 Java 程序
在每一个程序文件的起始处,你都必须使用 import 语句将所有额外的类导入到文件中。这里说 “额外” 是因为,所有 Java 文件都会自动导入一个特定的类库:java.lang
。
java.lang.System
类拥有几个字段,其中一个字段 out
是一个 static PrintStream
对象。因为 out
是 static
修饰的,所以无须借助 new
关键字,也就是说 out
关键字一直存在,直接使用即可。PrintStream
中有一个 println
方法,其作用是“在控制台打印我发送给你的内容,然后另起一行”。
需要注意的是,文件中必须存在一个与该文件同名的类(如果没有,编译器会报错)。此外如果需要创建一个能够独立运行的成,那么与文件同名的类中还必须包含一个程序启动的入口方法 :public static void main(String[] args)
public
关键字代表这个方法可以被外部程序所调用。main
的参数是一个 String
对象的数组,虽然目前我们并不会使用 args
参数,但 Java
编译器会强制你传递该参数,因为它用于获取控制台的输入。
下面代码将打印当前时间:
System.out.println(LocalDateTime.now());
在这段代码中,我们创建了一个作为参数的 LocalDateTime
对象,并将它的值传递给 println
方法。而当这一段语句执行完毕时,LocalDateTime
对象就没用了,因此垃圾回收器就可以随时清理它,而我们无须关心这种清理工作。
当查看 JDK
文档时,会发现 System
类包含了许多使用的方法(Java重要资产之一就是内容极为丰富的类库),比如:
public class ShowProperties {
public static void main(String[] args) {
System.getProperties().list(System.out);
System.out.println(System.getProperty("user.name"));
}
}
System.getProperties().list(System.out);
用于展示运行此程序的操作系统的所有属性,即操作系统的环境信息,并通过 list 方法将结果传递给参数 System.out
。list 方法还可以将结果发送到任何地方,比如发送到一个文件。
System.out.println(System.getProperty("user.name"))
用于获取一个特定的属性,如 user.name
或者是 java.library.path
8.1 编译和运行
编译:javac xxx.java
运行:java xxx
9、编程风格
驼峰命名法:类名首字母大写,其它内容:方法、字段(成员变量)、对象名首字母小写。
需要注意的是,你定义的冗长的名称也会影响到你的用户,所以请手下留情。
10、总结
本章的知识点足以让你理解如何编写一个简单的Java程序。此外,本章还介绍了 Java 语言的概况,以及一些语言基础。然而到目前为止,你所接触的代码示例都是“这么做,再那么做,然后再那么做”。接下来将会学习 Java 编程的基础运算符,并展示如何控制程序流程。