《On Java 中文版 基础卷》阅读笔记 对象无处不在 03

发布时间:2026/9/28 4:46:47
《On Java 中文版 基础卷》阅读笔记 对象无处不在 03 《On Java 中文版 基础卷》阅读笔记 对象无处不在 03“学习一门新语言将为你打开一扇通往新世界的大门。​” —路德维希·维特根斯坦虽然源于 C, Java 却是一门更为 “纯正” 的面向对象语言。C 和 Java 都是混合型语言混合型编程语言允许采用多种编程风格然而 Java 的设计者认为这种 “混合” 的重要性并不如在 C 中那么高。而 C 之所以是混合型的是因为要向后兼容 C 语言。由于 C 是 C 语言的超集因此它也包含了 C 语言的一些不良特性结果就是有时候 C 会显得过于复杂。Java 语言默认你只编写面向对象程序。所以在你准备大干一场之前需要先把自己的思维沉浸在面向对象的世界中。在本章中你将学习构建 Java 程序的基础组件并认识 Java 中的一切都是对象这个事实。3.1 通过引用操作对象“名字有什么意义呢就算用别的名字称呼玫瑰也丝毫不减她的芬芳。​” —威廉·莎士比亚​《罗密欧与朱丽叶》所有编程语言都在内存中处理各种元素。有时候程序员必须小心处理这种内存操作。比如你是直接操作这些元素还是通过某些特殊方式间接进行操作比如 C 和 C 中的指针。Java 将一切都视为对象。虽然说的是 “将一切都视为对象”​但你实际操作的其实是该对象的引用 (reference)。举个例子假设有一台电视机对象和一个遥控器引用​只要你掌握了遥控器这个引用就能控制电视机这个对象。当你想要 “换频道” 或者 “降低音量” 时你实际操作的是引用再由引用来修改对象信息。所以当你在房间里走来走去时只要带着遥控器即引用就依然可以遥控电视机。同样遥控器也可以不依赖于电视机而独立存在。也就是说引用未必会关联某个对象。比如若想保存一个词或一个句子你就需要创建一个String 类型的引用如下所示Strings;不过这样你只是创建了一个引用并没有创建一个对应的对象。如果现在你向变量 s 发送信息则会得到一条报错消息因为 s 还没有连接到任何对象也就是还没有电视机呢​。所以一种安全的做法是始终在创建引用时就进行初始化比如Stringsasdf;上面的代码使用了 Java 的一个特性即字符串 (String) 可以用带引号的文本进行初始化。而对于其他类型的对象而言你需要使用一种更为通用的初始化方式。3.2 必须创建所有对象引用的作用是关联对象。通常使用 new 关键字来创建对象。new 关键字表达的意思是 “创建一个这种类型的对象”​。所以在之前的例子中你可以这样编写代码StringsnewString(asdf);这一行不仅代码代表了 “创建一个新的字符串”​同时演示了如何通过一组字符来创建 String 对象。除了 String 以外Java 还提供了大量的预置类型可供选择。此外你还可以创建新类型。实际上创建新类型是 Java 编程的一项基础操作你将在本书的后续章节中学习相关内容。3.2.1 数据保存在哪里当程序运行时能够可视化其数据的排布方式是十分有帮助的对于内存管理来说尤其如此。下面列举了 5 种数据存储方式。1.寄存器 (register)。这是速度最快的数据存储方式因为它保存数据的位置不同于其他方式数据会直接保存在中央处理器 (central processing unit, CPU) 里。然而寄存器的数量是有限的所以只能按需分配。此外你不能直接控制寄存器的分配甚至你在程序中都找不到寄存器存在过的证据C 和 C 是例外它们允许你向编译器申请分配寄存器​。2.栈 (stack)。数据存储在随机存取存储器 (random-access memory, RAM) 里处理器可以通过栈指针 (stack pointer) 直接操作该数据。具体来说栈指针向下移动将申请一块新的内存向上移动则会释放这块内存。这是一种极其迅速和高效的内存分配方式其效率仅次于寄存器。只不过 Java 在创建应用程序时就必须明确栈上所有对象的生命周期。这种限制约束了程序的灵活性因此虽然有一些数据会保存在栈上尤其是对象引用​对象本身却并非如此。3.堆 (heap)。这是一个通用的内存池使用的也是 RAM 空间​用于存放所有 Java 对象。与栈不同的是编译器并不关心位于堆上的对象需要存在多久。因此堆的使用是非常灵活的。比如当你需要一个对象时可以随时使用 new 来创建这个对象那么当这段代码被执行时Java 会在堆上为该对象分配内存空间。然而这种灵活性是有代价的分配和清理堆存储要比栈存储花费更多的时间​。好消息是随着时间的推移Java 的堆内存分配机制已经变得非常高效了所以你并不需要太过关注此类问题。4.常量存储 (constant storage)。常量通常会直接保存在程序代码中因为它们的值不会改变所以这样做是安全的。有时候常量会与其他代码隔离开来于是在某些嵌入系统里这些常量就可以保存在只读存储器 (read-only memory, ROM) 中。5.非 RAM 存储 (non-RAM storage)。如果一段数据没有保存在应用程序里那么该数据的生命周期既不依赖于应用程序是否运行也不受应用程序的管制。其中最典型的例子之一是 “序列化对象”(serialized object)它指的是转换为字节流序列化​并可以发送至其他机器的对象。另一个例子则是 “持久化对象”(persistent object)它指的是保存在磁盘上的对象这些对象即便在程序结束运行之后也依然能够保持其状态。这些数据存储类型的特点在于它们会将对象转换成其他形式以保存于其他媒介中然后在需要的时候重新转换回常规的 RAM 对象。Java 支持轻量级的持久化对象存储而 JDBC 以及 Hibernate 等库则提供了更为成熟的解决方案即支持使用数据库存取对象信息。3.2.2 特殊情况基本类型有一些你经常使用的类型享受特殊待遇你可以将它们称为 “基本类型”(primitive type)。它们之所以享受特别待遇是因为对于基本类型Java 使用了与 C 以及 C 相同的实现机制这意味着无须使用 new 来创建基本类型的变量而是直接创建一个自动变量注意不是引用。也就是说该变量会直接在栈上保存它的值因此运行效率较高。new 关键字是在堆上创建对象这就意味着哪怕是创建一些简单的变量也不会很高效。Java 定义了每一种基本类型所占用的空间大小。即便是在不同的机器上这些类型所占用的空间也是保持一致的。而这种一致性也是 Java 程序比其他语言程序移植性更好的原因之一。数据类型字节数位数取值范围 / 说明byte1 字节8 位-128 ~ 127short2 字节16 位-32,768 ~ 32,767int4 字节32 位-2³¹ ~ 2³¹-1long8 字节64 位-2⁶³ ~ 2⁶³-1float4 字节32 位IEEE 754 单精度浮点数double8 字节64 位IEEE 754 双精度浮点数char2 字节16 位0 ~ 65,535Unicode 字符boolean未明确定义—仅true或false表中的所有数值类型都是有符号的所以不用在表格中查找无符号类型了。boolean 类型的空间大小没有明确标出其对象只能被赋值为 true 或 false。此外Java 还为基本类型提供了对应的 “包装类” (wrapper class)通过包装类可以创建与基本类型对应的堆空间对象。例如charcx;CharacterchnewCharacter(c);也可以这样表示CharacterchnewCharacter(x);而 “自动装箱” (autoboxing) 机制能够将基本类型自动转换为包装类对象例如Characterchx;也可以再转换回来例如Characterchx;charcch;至于使用包装类的原因后续章节会进行更为细致的讲解。高精度数字Java 提供了两个支持高精度计算的类分别是 BigInteger 和 BigDecimal。虽然这两个类也大致可以被归为包装类但是它们其实并没有对应的基本类型。这两个类都提供了一些方法来模拟基本类型的各种操作。也就是说你能对 int 和 float 做什么就能对 BigInteger 和 BigDecimal 做什么区别只是你用方法代替了运算符而已。此外由于涉及更多的计算量导致的结果就是相关操作的效率有所降低。所谓 “速度换精度” 即是如此。BigInteger 可以支持任意精度的整数。也就是说在运算时你可以表示任意精度的整数值而不用担心丢失精度。BigDecimal 可用于任意精度的定点数 (fixed-point number)。例如你可以将其用于货币计算。**定点数fixed-point number是小数点位置固定、用固定数量的整数位和小数位表示的数值属于计算机中的一种数值表示方式。**它可以表示整数也可以表示带小数的数当小数位数为 0 时本质上就是整数表示。小数点是怎么固定的计算机通常把定点数存成一个整数再约定其中有多少位用于表示小数。实际数值由这个约定还原出来十进制定点数保留 2 位小数时存储值 12345 可表示 123.45二进制定点数保留 4 个小数位时实际值通常等于存储的整数除以 2⁴因此同样的存储整数在小数位约定不同的情况下可能代表不同的数。定点数的关键不在于小数点真的被单独存储而在于程序和数据格式共同约定它的位置。和浮点数有什么区别浮点数floating-point number的小数点位置可以随着数值大小变化能表示很大或很小的数但运算结果可能出现舍入误差处理速度和硬件支持也取决于具体环境。定点数的小数位固定数值范围通常较小却有计算简单、结果更容易预测、资源占用较低等特点所以常用于嵌入式设备、信号处理、控制系统以及需要按固定小数位处理的金额或计量数据。如果你想了解这两个类的更多细节请参考 JDK 文档的相关描述。3.2.3 Java 中的数组许多编程语言都支持数组。然而在 C 和 C 里因为数组的本质是内存块所以使用数组是十分危险的。也就是说如果 C 程序访问了数组边界之外的内存或者在内存被初始化之前就对其进行操作这个问题非常普遍​那么结果如何就难以预料了。Java 的一个核心设计目的是安全于是许多折磨 C 和 C 程序员的问题在 Java 里已经不复存在了。例如Java 的数组一定会被初始化并且无法访问数组边界之外的元素。这种边界检查的代价是需要消耗少许内存以及运行时需要少量时间来验证索引的正确性。其背后的假设是安全性以及生产力的改善完全可以抵消这些代价。当你创建一个用于放置对象的数组时实际上数组里包含的是引用而这些引用会被自动初始化为一个特殊的值null。Java 会认为一个值为null 的引用没有指向任何对象所以当你操作引用之前需要确保将其指向了某个对象。如果你试图操作一个值为 null 的引用系统会返回一个运行时报错。因此Java 通过以上手段规避了那些常见的数组问题。此外你也可以创建一个放置基本类型的数组。编译器会确保对该数组进行初始化并将数组元素的值设置为0。本书后续尤其在第 21 章中会对数组进行更为详细的介绍。3.3 注释Java 支持两种类型的注释。第一种注释是传统的 C 语言风格即以 /* 开始中间可以有多行的注释内容然后以 */ 结束。需要注意的是许多程序员在书写多行注释时习惯于在每一行的开头使用 *所以你可能会经常见到如下代码/** * ClassName: Person * Package: com.yangjunbo.spring.definition.examplea * Description: * * Author 杨钧博 * Create 2026/9/7 8:42 * Version 1.0 */不过请切记所有位于 /* 和 */ 之间的内容都会被当作注释从而被编译器忽略所以即使这样写效果也是一样的/* This is a comment that continues across lines */第二种注释源于 C 语言风格。这种注释以 // 开始到同一行的末尾结束因此是一种单行注释。因为这种注释简单易用所以使用频率很高。以后你会经常看到类似这样的注释// This is a one-line comment3.4 无须销毁对象对于有些编程语言而言管理数据的生命周期需要花费不少精力。例如某个变量需要存在多长时间如果你需要销毁它应该何时动手呢数据的生命周期所造成的困扰会导致大量的 bug。本节将展示 Java如何帮你释放内存并大幅简化这些问题。3.4.1 作用域大多数过程式编程语言具有作用域 (scope) 的概念。作用域会决定其范围内定义的变量的可见性和生命周期。C、C 以及 Java 的作用域范围都是通过大括号 ({}) 来定义的下面是一个 Java 作用域的例子{intx12;// 在这里只有变量 x 可见{intq96;// 在这里变量 x 和 q 都可见}//在这里超出了变量 q 的作用域//只有变量 x 可见}在作用域里定义的变量只在该作用域的范围内可见。顺便一提代码缩进可以提高 Java 代码的可读性。由于 Java 是自由形态的编程语言所以多余的空格、tab 缩进、回车符等都不会影响程序的运行效果。此外虽然下面的代码对于 C 和 C 而言是合法的但在 Java 中不能这样使用{intx12;{intx96;//语法错误}}Java 编译器会提示说变量 x 已经定义过了。因此类似 C 和 C 那样在外围的作用域中 “隐藏” 变量的方式在 Java 中是不被允许的因为Java 的设计者认为这种编程方式会引发歧义。3.4.2 对象的作用域Java 对象的生命周期和基本类型有所不同。当你使用 new 创建一个对象时该对象在其作用域结束后会依然存在。因此如果你编写如下代码{StringsnewString(a string);}虽然引用 s 会在作用域结束后消失但是它指向的 String 对象还会继续占用内存。对于上面的代码而言该 String 对象在作用域结束后就无法再获取这是因为已经超出了其唯一引用的作用域范围。在后续章节中你将看到如何在程序中传递和复制对象的引用。如果需要的话通过 new 创建的对象会存在足够长的时间因此 C 编程中出现的一系列问题在 Java 中就消失了。在编写 C 代码时你不仅要确保对象在需要的时候随时可用而且事后还要负责销毁这些对象。等等还有个问题。如果 Java 对这些对象不闻不问为什么应用程序并没有因为内存溢出而强行结束呢而这一点也正是 C 存在的问题。原因就在于Java 使用了一点魔法即垃圾收集器会监视所有通过 new 创建的对象并及时发现哪些对象不再被引用然后它会释放这些对象所占用的内存使得这些内存可以用于其他新对象。这意味着你无须关注何时需要释放内存你要做的只是创建对象而已如果你不再需要这些对象就任由它们去吧垃圾收集器会处理它们​。这种机制解决了一类非常严重的编程问题由于程序员忘记释放内存而导致的 “内存泄漏” (memory leak)。3.5 使用 class 关键字创建新类型如果一切皆为对象那么具体到某一类对象的特征和行为是如何确定的呢换句话说如何创建对象的类型说到这里你可能会想当然地认为会有一个名为 “type” 的关键字然而由于历史原因大多数面向对象编程语言会使用 “class” 关键字来描述新的对象类型。其用法是 class 关键字后面跟着新的类名classATypeName{//类的具体特性和行为}这段代码创建了一个新的对象类型即类只不过其内容只包含了一行注释因此目前也没有什么实际的作用。话虽如此你还是可以通过 new 关键字创建一个该类的对象ATypeNameanewATypeName();除非你为这个类定义了一些方法否则到目前为止这个类能做的事情并不多。字段当定义一个类时你可以为其定义两种元素字段有时叫作 “成员数据”​和方法有时叫作 “成员函数”​​。通过对象的引用与字段进行交互。字段可以是任何类型的对象也可以是基本类型。如果一个字段是某个对象的引用你必须通过 new 关键字初始化该对象并将其关联到具体的引用上。每一个对象都会单独保存其字段。通常来说不同对象的字段之间不会共享。下面的代码是一个定义了字段的类classDataOnly{inti;doubled;booleanb;}这个类只是定义了几个数据字段而已。和之前一样你可以这样创建一个该类的对象DataOnlydatanewDataOnly();你可以通过对象成员为字段赋值。具体做法是先写出对象的引用名跟着再写一个 “.”​然后是对象的成员名objectReference.member例如可以这样为字段赋值data.i47;data.d1.1;data.bfalse;如果一个对象包含了其他对象而你想要修改其他对象的数据该怎么办同样可以使用 “.” 来实现例如myPlane.leftTank.capacity100;理论上你可以用这种方法嵌套无穷多的对象但是需要提醒的是这种设计方式并不优雅​。基本类型的默认值当一个类的字段是基本类型时即便你没有初始化这些字段它们也会拥有默认值如表所示。数据类型默认值byte0short0int0long0Lfloat0.0fdouble0.0dchar\u0000空字符booleanfalse这一特点确保了基本类型的字段一定会被初始化C 就不会这么做​并且从源头减少了许多不应该出现的 bug。只不过对于你编写的程序而言这些默认值可能并不是正确或合理的值所以最佳实践应该是你显式地初始化这些变量。此外这种机制并不会应用于局部变量 (local variable)因为局部变量并不是类的字段。因此如果你在一个方法内部编写如下代码intx;那么变量 x 不会自动被初始化为 0。因此在使用变量 x 之前你必须为其赋值以确保正确性。如果你忘记了赋值Java 的处理方式明显要比 C 更好一些— Java 会抛出一个编译错误以告知你变量没有被初始化相比之下C 编译器通常只是警告你有些变量没有被初始化而 Java 则是直接抛出错误​。3.6 方法、参数以及返回值在许多编程语言比如 C 和 C中​“函数” (function) 用于表示子程序。而在 Java 中称之为 “方法”(method)意思是 “做某件事的方式”​。Java 中的方法决定了对象具有哪些行为可以干什么。方法最基础的几个部分包括方法名、参数、返回值以及方法体 (method body)。例如ReturnTypemethodName(/* 参数列表 */){// 方法体}ReturnType 表示当调用一个方法时该方法返回的值是什么类型。参数列表提供了一系列需要传递给方法的数据包括数据的类型和名称。方法名和参数列表共同构成了方法的 “签名”(signature)方法签名即该方法的唯一标识符。Java 中的方法只能作为类的一部分而存在方法只能通过对象调用。如果你通过对象调用了一个不属于该对象的方法会得到一个编译时错误。调用对象方法的具体方式为在对象引用之后添加一个 “.”​然后紧跟着方法名及其参数列表objectReference.methodName(arg1,arg2,arg3);现在思考一下假设有对象 a 定义了 f() 方法如何调用一个无参数且返回 int 值的 f() 方法。可以这样编写代码intxa.f();需要注意的是返回值的类型必须与变量 x 的类型保持一致。有时候这种调用方法的行为也被描述为 “向一个对象发送一条消息”​。在上面的例子中f() 代表消息a 代表对象。此外也可以将面向对象编程描述为 “向对象发送消息”​。参数列表参数列表描述了需要传递给方法的数据。也许你已经猜到了这些信息和 Java 中的其他内容一样都是对象。所以参数列表也需要描述这些对象的类型以及对象名。和之前一样当你操作对象时实际操作的是它的引用。此外引用的类型必须正确如果方法定义的参数是 String 类型那就必须传递一个 String 对象给该方法否则编译器会报错。下面的例子是一个接受 String 类型参数的方法intstorage(Strings){returns.length()*2;}上面的方法计算并返回了保存指定字符串所需的字节数。参数 s 的类型是 String。一旦 s 被传递至 storage() 方法内部就可以像操作其他对象一样操作它也就是可以向它发送消息。这里调用了它的 length() 方法这个属于 String 类的方法将返回指定字符串的字符长度。字符串的每一个字符长度是 16 位也就是两个字节。而 return 关键字做了两件事。第一件事是告诉程序 “从方法中离开吧一切都结束了”​。第二件事是当该方法生成了一个值的时候将这个值放置于 return 之后。在上面的例子里最后生成的返回值是计算 s.length()*2 的结果。你可以返回任意类型的值但是如果不返回任何值就表示该方法生成的值为 void意思是什么都没有​booleanflag(){returntrue;}doublenaturalLoaBase(){return2.718;}voidnothing(){return;}voidnothing2(){}当返回值的类型是 void 时使用 return 关键字的作用是退出该方法因此在方法的末尾处就没有必要使用 return 了。此外你可以在方法的任意位置使用 return。然而一旦返回值是非 void 类型无论你在何处返回编译器都会强制你必须提供正确类型的返回值。如此看来Java 程序似乎就是一群对象将其他对象设置为其方法的参数然后向这些对象发送消息。到目前为止情况确实如此不过在第 4 章中将介绍如何通过方法执行一些更为底层的操作。对于本章而言发送消息就足够了。发送消息的意思是调用对象的方法。3.7 编写 Java 程序3.7.1 名称可见性所有的编程语言都有一个问题那就是对名称的控制。比如你在程序的一个模块中使用了一个名称有人在其他模块中也使用了这个名称那么应该如何区分两者又该如何避免两者之间的“冲突”​对于 C 语言来说这个问题尤其令人感到头疼因为程序中经常充斥着各种名称维护成本极高。而 C 的类Java 中类的设计参考了 C则是将函数放置于类的内部因此一个类的函数名不会与其他类的同名函数发生冲突。但是C 依然允许使用全局数据和全局函数所以潜在的冲突也依然存在。为了解决这个问题C 使用额外的关键字引入了命名空间的概念。而 Java 通过一种新颖的方法解决了此问题。为了能够清晰地描述库的名称Java 语言的设计者所使用的方法是将你的互联网域名反转使用。因为域名是唯一的所以一定不会冲突。举例来说如果域名是 ituring.com那么 foibles 库的名称就是 com.ituring.utility.foibles。而反转的域名之后的几个 “.” 实际上描述了子目录的结构。在 Java 1.0 和 Java 1.1 里按照约定俗成的惯例类似 com、edu、org、net 等扩展名的首字母都是大写所以你会看到类似Com.ituring.utility.foibles 这样的名称。然而在 Java 2 的开发过程中Java 的设计者发现这种方法会引发一些问题。于是从那之后一直到现在包名都变成了小写字母。这种机制确保了所有文件都有对应的命名空间同时文件里定义的类都具有唯一对应的标识符。因此Java语言就避免了命名冲突的问题。使用反转的 URL 来定义命名空间是一种全新的尝试在此之前没有其他语言使用过此方案。此外Java 语言还有其他一些与之类似的解决特定问题的方案。不难想象如果一种新特性没有经过充分验证的话将来出现问题的概率会很大。如果在实际编程阶段应用了该特性一旦出现问题就回天乏术了有些问题甚至严重到需要从语言中去掉某些特性​。将命名空间和利用反转的URL所生成的文件路径相关联会带来一个问题不过这个问题并不是程序的 bug而是会对管理源代码带来一些挑战。比如想要使用 com.ituring.utility.foibles 这个命名空间那么就需要创建名为 com 和 ituring 的空文件夹其目的只是为了与反转的 URL 相对应。而这种做法还将引入一个你之后在 Java 工程中会遇到的情况通过空文件夹层层嵌套而形成的这种文件夹结构其作用不只是为了对应反转的URL同时这些冗长的文件夹路径通常用于表明文件夹内容。因此在编写工程级别的 Java 代码时基本上你不得不使用那些精心设计的 IDE 以管理层层嵌套的代码比如 IntelliJ IDEA 等。由于这种深不见底的文件夹结构会让你在开始学习语言时就想要使用大型 IDE而我并不想让你承受这种额外的负担和困扰因此本书所有章节的代码示例都使用了浅显易懂的结构即子文件夹名与其英文版章节名保持一致。3.7.2 使用其他组件当你在程序中使用其它类时编译器必须找到这个类。最简单的情况是这个类就存在于当前被调用的源代码文件中。如此一来你只要使用这个类就可以了。那么如果一个类定义在其他文件里呢可能你会认为万能的编译器会自动找到这个类然而问题并非如此简单。比如你想使用一个类却发现同一个类名存在多个不同的定义假设这些定义的用途各不相同​。更有甚者假设你正在编写一个程序当你向库中添加了一个新的类时可能会发现已经和库中一个已有的类名冲突了。为了解决这个问题需要消除所有类似这样的潜在歧义解决方案则是利用 import 关键字告知 Java 编译器你想要使用哪个类。import 语句的作用是通知编译器导入一个指定位置的包即放置各种类的库其他编程语言的库可以由函数、数据和类组成但是 Java 的一切活动都发生在类中​。你经常会用到编译器自带的各种 Java 标准库组件。当你使用这些组件时无须担心那些冗长和反转的域名使用下面的方式即可将其导入importjava.util.ArrayList;这一行代码会告诉编译器你要使用位于 util 库的 ArrayList 类。此外util 库也包括了一些其他的类可能你需要使用其中的几个类却不想一个个地导入它们。这种情况下可以使用通配符 “*” 来轻松实现这一点importjava.util.*;由于本书的代码示例规模都很小为了方便起见经常会使用 “*” 导入类。不过需要提醒的是许多编程风格指南会明确指出每一个用到的类都应该被单独导入。3.7.3 static 关键字创建一个类即描述了其对象的特征和行为。直到使用 new 关键字时你才会真正创建一个对象以及为该对象分配内存并使得其方法可以被调用。然而在两种情况下这种方式会显得不合时宜。第一种情况是有时候需要一小块共享空间来保存某个特定的字段而并不关心创建多少个对象甚至有没有创建对象。第二种情况是你需要使用一个类的某个方法而该方法和具体的对象无关换句话说你希望即便没有生成任何该类的对象依然可以调用其方法。static 关键字源自 C为以上两个问题提供了解决方案。如果你使用了 static 关键字则意味着使用 static 的字段或方法不依赖于任何对象。也就是说即便你没有为一个类创建任何对象依然可以调用该类的 static 方法或 static 字段。另外由于非 static 的字段和方法必须基于指定的对象因此对于非 static 的字段和方法来说你必须创建一个对象才可以使用非 static 的字段或方法。有些面向对象编程语言也会使用 “类数据” (class data) 和 “类方法” (class method) 来表示该数据或方法只服务于类而非特定的对象。有时候Java 也会使用这两种称谓。创建一个 static 字段或方法只需要把 static 关键字放置于字段或方法定义的前面就可以了。以下是创建并初始化一个 static 字段的例子classStaticTest{staticinti47;}即便你创建了两个 StaticTest 类的对象StaticTest.i 依然只会占用同一块内存空间。也就是说字段 i 会被两个对象所共享。例如StaticTestst1newStaticTest();StaticTestst2newStaticTest();st1.i 和 st2.i 的值都是 47这是因为两者使用的内存空间是相同的。有两种方法可以调用 static 变量。其一是通过对象来调用比如之前例子中的 st2.i。此外你也可以直接通过类名来调用非 static 成员则不能这样使用​StaticTest.i;这里出现的 运算符的作用是将变量的值加 1。结果就是st1.i 和 st2.i 的值都变成了 48。推荐通过类名调用 static 变量因为这种方法突出了变量的 static 特质。类似的逻辑也适用于 static 方法。你既可以通过对象来调用 static 方法和非 static 方法一样​也可以使用专用语法 ClassName.method()即类名加方法名。例如你可以这样定义一个 static 方法classIncrementable(){staticvoidincrement(){StaticTest.i;}}increment() 方法通过 运算符将 static 整数 i 的值加 1。你可以使用传统方式即通过对象调用 increment() 方法IncrementablesfnewIncrementable();sf.increment();不过推荐的做法是直接通过类名来调用Incrementable.increment();将 static 关键字应用于字段毫无疑问会改变其数据生成的方式即 static 字段是基于类创建的非 static 字段则是基于对象创建的。而将 static 应用于方法时即便你没有创建对象也可以调用该方法。你之后会看到作为运行应用程序的入口在定义 main() 方法时static 关键字是必不可少的一部分。3.8 你的第一个 Java 程序现在你终于迎来了编写第一个完整程序的时刻。该程序运行时将显示一个字符串以及一个通过 Java 标准库的 Date 类所生成的日期。// objects/HelloDate.javaimportjava.util.*;publicclassHelloDate{publicstaticvoidmain(String[]args){System.out.println(Hello, its: );System.out.println(newDate());}}在本书的代码示例中第一行注释都具有特别的作用显示文件夹路径以及文件名。此外通过第一行注释你也可以很方便地在示例代码中找到对应的源代码。在每一个代码文件的起始处你都必须使用 import 语句将所有额外的类导入到文件中。这里说 “额外” 是因为所有 Java 文件都会自动导入一个特定的库即 java.lang。你可以打开浏览器在 Oracle 网站上查看这个库的文档如果你还没有下载 JDK 文档现在就可以去 Oracle 公司的 Java 网站下载或者通过互联网搜索和查看。然后如果你查看包的列表会看到各种类的库。比如你可以选择 java.lang你会看到一个呈现了该库所有类的列表。由于所有的 Java 代码文件都默认导入了 java.lang所以列表中的类都可以直接调用。另外由于 java.lang 并不包含 Date 类这就意味着你必须从其他库将其导入后才能使用。如果你不知道一个特定的类属于哪个库或者说你想查看所有的类可以点击 Java 文档页面的 “Tree” 按钮页面会显示 Java 中所有的类供你查询。然后你就可以通过浏览器的查找功能来找到 Date 类。比如你会在页面列表中找到 java.util.Date这意味着 Date 类包含在 util 库中。因此你必须在 Java 文件中添加一行代码 import java.util.*然后才可以使用 Date 类。JDK 官方文档如果你在文档页面选择了 java.lang 然后查看其中的 System 类会发现 System 类拥有几个字段。如果你选择了其中一个字段 out会发现这是一个 static PrintStream 对象。正因为它是 static 字段所以你无须借助 new 关键字。也就是说out 对象会一直存在你直接使用就行了。out 对象的用途取决于它的类型即 PrintStream。此外你会发现文档描述中的 PrintStream 是一个超链接可以点击该超链接来查看 PrintStream 都提供了哪些方法为我们所用。其中包含的方法数量还不少会在后续章节中陆续提及。现在需要的是 println() 方法其作用是 “在控制台打印我发送给你的内容然后另起一行”​。因此如果你需要在控制台显示一段信息可以在Java 程序中编写如下代码System.out.println(A String of things);需要注意的是文件中的类名必须与文件同名如果你没有这么做则编译器会报错​。此外如果你需要创建一个能够独立运行的程序那么与文件同名的类中还必须包含一个程序启动的入口方法。这个特殊的方法叫作 main()具有以下的格式和返回值publicstaticvoidmain(String[]args){}public 关键字代表这个方法可以被外部程序所调用相关详细内容会在第 7 章中进行介绍​。main() 的参数是一个 String 类型的数组虽然目前并不会使用 args 参数但 Java 编译器会强制你定义该参数因为它用于获取控制台的输入。下面的代码将打印当前的日期System.out.println(newDate());在这段代码中创建了一个作为参数的 Date 对象并将它的值传递给 println() 方法。而当这一段语句执行完毕时Date 对象就没用了因此垃圾收集器可以随时清理它而程序员则无须关心这种清理工作。当你查看 JDK 文档时会发现 System 类包含了许多实用的方法Java 的重要资产之一就是内容极为丰富的标准库​比如// objects/ShowProperties.javapublicclassShowProperties{publicstaticvoidmain(String[]args){System.getProperties().list(System.out);System.out.println(System.getProperty(user.name));System.out.println(System.getProperty(java.library.path));}}main() 方法的第一行用于展示运行此程序的操作系统的所有属性即操作系统的环境信息并通过 list() 方法将结果传递给参数 System.out。你会在本书的后续内容中看到你还可以将结果发送至任何地方比如发送至一个文件。同时你也可以获取一个特定的属性比如这里获取的是 user.name 以及 java.library.path。编译和运行为了能够编译和运行本书中的程序你必须搭建好 Java 编程环境其安装过程在第 2 章有详细的介绍。如果你遵从了指引的步骤你使用的应该是从 Oracle 网站上免费获取的 Java 开发者工具集(Java Developer’s Kit, JDK)。此外第 2 章也介绍了如何安装本书的代码示例。安装完成后请打开名为 objects 的子目录并通过命令行输入javac HelloDate.java这一行代码应该不会输出任何内容。如果你看到了报错消息就意味着你没有正确安装 JDK那么你需要调查问题原因并解决它。如果命令行没有报错那么你可以输入javaHelloDate结果就是你会看到当前日期输出到了控制台。这就是编译和运行本书所有程序的过程。另外本书源代码的根目录里还包含了一个叫作 build.gradle 的文件此文件包含了自动化构建、测试以及运行本书代码文件的 Gradle 的配置。当你初次运行 gradlew 命令时Gradle 会被自动安装前提是你已经安装了 Java​。3.9 编程风格“Code Conventions for the Java Programming Language Java 编程语言代码规范” 中定义的代码风格是类名首字母要大写。如果类名包含多个词也要把这些词汇总到一起也就是说不再使用下划线作为词语之间的间隔​每一个词的首字母也要大写例如classAllTheColorsOfTheRainbow{//...这种方式叫作 “驼峰式命名法” (Camel-Case)。而其他内容比如方法、字段成员变量​以及对象名等除了首字母要小写以外使用的命名方式和类名一样。例如classAllTheColorsOfTheRainbow{intanIntegerRepresentingColors;voidchangeTheHueOfTheColor(intnewHue){// ...}// ...}需要注意的是你定义的冗长的名称也会影响使用所以请手下留情。3.10 总结本章讲述的知识点足以让你理解如何编写一个简单的 Java 程序。此外本章还介绍了 Java 语言的概况以及一些语言基础。然而到目前为止你所接触的代码示例都是 “这么做再这么做然后再那么做”​。在接下来的两章里会介绍 Java 编程的基础运算符并展示如何控制程序流程。