虽然源于C++,Java却是一门更为“纯正”的面向对象语言。
Java和C++都是混合型语言,混合型编程语言允许采用多种编程风格。
Java语言预期只编写面向对象程序。在本章,你将学习构建Java程序的基础组件,并认识Java中(几乎)一切都是对象这个事实。
所有编程语言都在内存中处理各种元素。
String s;
这样只是创建了一个引用而非对象。此时向对象s发送消息,则会得到一条报错信息NullPointException,因为此时s还没有连接到任何对象。
一种安全做法:始终在创建对象时进行初始化。
String s = "asdf";
此处使用了Java的一个特性:字符串(String)可以用带引号的文本进行初始化。而对于其它类型的对象而言,需要使用 new 进行初始化。(更为通用)
new关键字表达的意思是“我要一个这种类型的对象”。
String s = new String("asdf");
这一行代码不仅代表了“创建一个新的字符串”,同时也告诉我们如何如何通过一组字符来创建String对象。
有些经常使用的类型享受特殊待遇,可以将它们称为“基本类型”(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;
Java中提供了两个支持高精度计算的类:精度换速度
详细看附录2:02.附录:Java中Math、BigInteger、BigDecimal详解
Java的一个核心设计目的是安全,于是Java解决了C和C++使用数组的许多问题:
这种边界检查的代价是需要消耗少许内存,以及运行时需要少量时间来验证索引的正确性。其背后的假设是:安全性和生产力的改善完全可以抵消这些代价,Java同时也会优化这些操作。
单行注释://
多行注释:/**/
数据的生命周期所造成的困扰会导致大量 bug。本节将展示 Java 如何释放内存。
作用域(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; // 语法错误
}
}
Java对象的生命周期和基本类型有所不同。当使用 new 创建一个对象,该对象在作用域结束后会依然存在。
{
String s = new String("a hhahh");
} // 作用域结束
虽然引用 s 会在作用域结束后会消失,但是它指向的String对象还会继续占用内存。
对于上面代码而言,该String对象在作用域结束后就无法再获取,因为已经超出了其唯一引用的作用域范围。如何在程序中传递和复制对象的引用?
如果需要的话,通过new创建的对象会存在足够长的时间,因为Java无须像C++一样,不仅要确保对象在需要的时候随时可用,而且事后还需要负责销毁这些对象。
Java的垃圾收集器会监视所有通过 new 创建的对象,并及时发现哪些对象不再被引用,然后它会释放这些对象所占用的内存,使得这些内存可以用于其它新对象。这种机制解决了一类非常严重的编程问题:由于程序员忘记释放内存而导致“内存泄漏”(memory leak)。
大多数面向对象编程语言会使用“class”关键字来描述新的对象种类。其用法就是 class 关键字后面跟着新的类名:
class ATypeName {
// 类的具体实现放在这里
}
上面代码创建了一个新的类,只不过其内容只包含一行注释,一次目前也没有什么实际的作用。话虽如此,你还是可以通过 new 关键字创建一个该类的对象。
ATypeName a = new ATypeName();
定义一个类,你可以为其定义两种元素:字段 和 方法。我们通过对象的引用与字段进行交互。字段可以是任何类型的对象,也可以是基本类型。如果一个字段是某个对象的引用,则必须通过 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;
理论上可以使用这种方法嵌套无穷多的对象,但是不够优雅。
当一个类的字段为基本类型时,即使没有对其进行初始化,它们也会拥有默认值:
基本类型 | 默认值 |
---|---|
char | \u0000(null) |
boolean | false |
byte | 0 |
short | 0 |
int | 0 |
long | 0L |
float | 0.0f |
double | 0.0d |
当变量作为类成员存在时,Java才会将其初始化为以上的默认值。这一点确保了基本类型的字段一定会被初始化。
只不过对于我们编写的代码而言,这些默认值可能并不是正确或合理的值,所以最佳的实践应该是显示初始化这些变量。
此外,这种机制并不会应用于局部变量(local variable),因为局部变量并不是类的字段。
Java中,一个局部变量如果未进行初始化就使用,Java会抛出一个编译错误以告知你变量没有被初始化。
在Java中,方法/函数(method/function)是“做某件事的方式/行为”。
Java中的方法决定了对象可以接受哪些消息。方法最基础的几个部分包括:方法名、返回值、参数,以及方法体。例如:
ReturnType methodName(/* 参数列表 */) {
// 方法体
}
ReturnType 表示当调用一个方法时,该方法生成的值是什么类型。参数列表提供了一系列需要传递给方法的信息,包括信息的类型和名称。方法名和参数列表共同构成了方法的“签名”,方法签名即该方法的唯一标识符。
Java中的方法只能作为类的一部分存在,方法只能通过对象调用,而该对象必须能执行该方法。如果通过对象调用一个不属于该对象的方法,编译报错。
调用方法的具体方式:对象.方法名及其参数列表:objectReference(arg1, arg2, arg3)
如果定义一个无参且返回int值的f()方法。假设有对象 a 定义了 f() 方法:int a = f();
注意: 返回值类型必须与接收变量的类型保持一致。
面向对象编程:向对象发送消息
参数列表描述了需要传递给方法的信息。这些信息和Java中其它内容一样都是对象。所以,参数列表也需要描述这些对象的类型和名称。和之前一样,操作对象时,实际操作的是它的引用。此外,引用类型必须正确,如果方法定义的参数是 String 类型,就必须传递一个 String 对象给该方法,否则编译报错。
方法必须定义在一个类的内部。
return 关键字的作用:
方法可以返回任意类型的值,但是如果不返回任何值,就表示该方法生成的值为 void。
当返回值的类型是 void 时,使用 return 关键字的作用是退出该方法,因此在方法的末尾处就没有必要使用 return 方法了。此外,可以在方法的任意位置使用 return。然而,一旦返回值是非 void 类型,无论在何处返回,编译器都会强制要求必须提供正确类型的返回值。
如此看来,Java 程序似乎就是一群对象将其他对象设置为其方法的参数,然后向这些对象发送消息。
所有的编程语言都有一个问题:对名称的控制——即防止名称冲突。
Java 为了能够清晰的描述库的名称,Java语言的设计者所使用的方法是将你的互联网域名反转使用。因为域名是唯一的,所以一定不会冲突。反转之后的域名之后的几个 “.” ,实际上描述了子目录的结构。
这种机制确保了所有文件都有对应的命名空间,同时文件里定义的类都具有唯一对应的标识符。因此,Java语言就避免了命名冲突的问题。
当你在程序中使用预定义的类时,编译器必须找到这个类。我们需要消除所有类似类名冲突这样的潜在歧义,解决方案则是利用 import 关键字告知 Java 编译器你想使用哪个类。import 语句的作用是通知编译器导入一个指定位置的包,即放置各种类的库。
开发中经常会用到编译器自带的各种 Java 标准库组件。当你使用这些组件时,无须担心哪些冗长和反转的域名,使用下面的方式即可导入:
import java.util.ArrayList;
这一行代码会告诉编译器,我们要使用位于 util 库的 ArrayList 类。
此外。util 库也包括了一些其它的类,可能你需要使用其中的几个类,却不想一个个地导入它们。这种情况下,可以使用通配符 “*” 来轻松实现这一点:
import java.util.*;
注意:许多编程风格指南明确指出:每一个用到的类都应该被单独导入。
创建一个类即描述了其对象的外观和行为。直到使用 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 应用于方法时,即便没有创建对象,也可以通过类名调用该方法。后面会看到,作为程序应用的入口,main 方法中 static 关键字是必不可少的一部分。
在每一个程序文件的起始处,你都必须使用 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
编译:javac xxx.java
运行:java xxx
驼峰命名法:类名首字母大写,其它内容:方法、字段(成员变量)、对象名首字母小写。
需要注意的是,你定义的冗长的名称也会影响到你的用户,所以请手下留情。
本章的知识点足以让你理解如何编写一个简单的Java程序。此外,本章还介绍了 Java 语言的概况,以及一些语言基础。然而到目前为止,你所接触的代码示例都是“这么做,再那么做,然后再那么做”。接下来将会学习 Java 编程的基础运算符,并展示如何控制程序流程。