《OnJava8》精读(二) 万物皆对象
@
介绍
《On Java 8》是什么?
它是《Thinking In Java》的作者Bruce Eckel基于Java8写的新书。里面包含了对Java深入的理解及思想维度的理念。可以比作Java界的“武学秘籍”。任何Java语言的使用者,甚至是非Java使用者但是对面向对象思想有兴趣的程序员都该一读的经典书籍。目前豆瓣评分9.5,是公认的编程经典。
为什么要写这个系列的精读博文?
由于书籍读起来时间久,过程漫长,因此产生了写本精读系列的最初想法。除此之外,由于中文版是译版,读起来还是有较大的生硬感(这种差异并非译者的翻译问题,类似英文无法译出唐诗的原因),这导致我们理解作者意图需要一点推敲。再加上原书的内容很长,只第一章就多达一万多字(不含代码),读起来就需要大量时间。
所以,如果现在有一个人能替我们先仔细读一遍,筛选出其中的精华,让我们可以在地铁上或者路上不用花太多时间就可以了解这边经典书籍的思想那就最好不过了。于是这个系列诞生了。
一些建议
推荐读本书的英文版原著。此外,也可以参考本书的中文译版。我在写这个系列的时候,会尽量的保证以“陈述”的方式表达原著的内容,也会写出自己的部分观点,但是这种观点会保持理性并尽量少而精。本系列中对于原著的内容会以引用的方式体现。
最重要的一点,大家可以通过博客平台的评论功能多加交流,这也是学习的一个重要环节。
第二章 安装Java和本书用例
本章总字数:2400
关键词:
- 选择一个自己用着舒服的编辑器(IDE)
- 安装Java环境
- 运行示例代码
本章是书的过度章节,主要介绍了如何选择适合的开发编辑器,以及安装配置Java配置环境。在书中,作者提到了“适合的就是最好的”概念。任何编辑都是为编程服务的,所以挑选一个自己用着舒服的编辑器即可。作者还着重推荐了Atom 文本编辑器(一个免费开源编辑器)。
在本章的最后部分,作者介绍了如何安装和运行Java。在这里就不作为重点讲解了。如果你是Java初学者可以参考《Java+IDEA环境配置整合》。需要说明的是,作者提到可以通过GitHub的OnJava8-Examples获取对应的教学实例代码。
第三章 万物皆对象
本章总字数:11000
关键词:
- 创建对象
- 数据存储
- 作用域
- 类的创建
- 引用组件
- static
- 第一个Java程序
对象操纵
作者依然对比C++语言,在C++中“对象的操纵是通过指针来完成的”。而Java中利用了“万物皆对象”的思想简化问题,所以使用了引用方式来操纵。
举例:我们可以用遥控器(引用)去操纵电视(对象)。只要拥有对象的“引用”,就可以操纵该“对象”。换句话说,我们无需直接接触电视,就可通过遥控器(引用)自由地控制电视(对象)的频道和音量。此外,没有电视,遥控器也可以单独存在。就是说,你仅仅有一个“引用”并不意味着你必然有一个与之关联的“对象”。
String s;
String s2 = "asdf";
前者s只是创建了一个对象的引用,而不是对象。如果直接使用会出错。而s2是创建了引用且同时进行了初始化。
“引用”用来关联“对象”
上方的代码也可以用new的方式来创建一个新对象。
String s = new String("asdf");
数据存储
数据存储部分是本章的关键内容。Java中数据可以存储在五个不同的地方。这五个地方各有不同:
寄存器(Registers)最快的存储区域,位于 CPU 内部。然而,寄存器的数量十分有限,所以寄存器根据需求进行分配。我们对其没有直接的控制权,也无法在自己的程序里找到寄存器存在的踪迹(另一方面,C/C++> 允许开发者向编译器建议寄存器的分配)。
栈内存(Stack)存在于常规内存RAM区域中,可通过栈指针获得处理器的直接支持。栈指针下移分配内存,上移释放内存,这是一种快速有效的内存分配方法,速度仅次于寄存器。创建程序时,Java 系统必须准确地知道栈内保存的所有项的生命周期。这种约束限制了程序的灵活性。因此,虽然在栈内存上存在一些 Java 数据,特别是对象引用,但 Java 对象却是保存在堆内存的。
堆内存(Heap)这是一种通用的内存池(也在 RAM 区域),所有 Java 对象都存在于其中。与栈内存不同,编译器不需要知道对象必须在堆内存上停留多长时间。因此,用堆内存保存数据更具灵活性。创建一个对象时,只需用 new 命令实例化对象即可,当执行代码时,会自动在堆中进行内存分配。这种灵活性是有代价的:分配和清理堆内存要比栈内存需要更多的时间(如果可以用 Java 在栈内存上创建对象,就像在 C++ 中那样的话)。随着时间的推移,Java 的堆内存分配机制现在已经非常快,因此这不是一个值得关心的问题了。
常量存储(Constant storage)常量值通常直接放在程序代码中,因为它们永远不会改变。如需严格保护,可考虑将它们置于只读存储器 ROM (只读存储器,Read Only Memory)中。
非 RAM 存储(Non-RAM storage)数据完全存在于程序之外,在程序未运行以及脱离程序控制后依然存在。两个主要的例子:(1)序列化对象:对象被转换为字节流,通常被发送到另一台机器;2)持久化对象:对象被放置在磁盘上,即使程序终止,数据依然存在。这些存储的方式都是将对象转存于另一个介质中,并在需要时恢复成常规的、基于 RAM 的对象。Java 为轻量级持久化提供了支持。而诸如 JDBC 和 Hibernate 这些类库为使用数据库存储和检索对象信息提供了更复杂的支持。
Java中常用的基本类型由存储在栈内存的各种数据类型构成,因此更加高效。
基本类型的几种使用方式:
char c = 'x';
Character ch = new Character(c);
Character ch1 = new Character('x');
Character ch2 = 'x';//自动装箱
char c = ch;//自动拆箱
在Java中,使用数组是安全的。Java对数组的使用有严格要求(相比C++),“数组使用前需要被初始化,并且不能访问数组长度以外的数据”。这使得数组的使用可以在安全性和效率上的得以提高。
当我们创建对象数组时,实际上是创建了一个引用数组,并且每个引用的初始值都为 null 。在使用该数组之前,我们必须为每个引用指定一个对象 。如果我们尝试使用为 null 的引用,则会在运行时报错。因此,在 Java 中就防止了数组操作的常规错误。
作用域
Java 的变量只有在其作用域内才可用。合法使用作用域:
{
int x = 12;
// 仅 x 变量可用
{
int q = 96;
// x 和 q 变量皆可用
}
// 仅 x 变量可用
// 变量 q 不在作用域内
}
以下则是非法的使用方式( C++ 中是合法的):
{
int x = 12;
{
int x = 96; // Illegal
}
}
对象的作用域:
{
String s = new String("a string");
}
// 作用域终点
Java 的垃圾收集器会检查所有 new 出来的对象并判断哪些不再可达,继而释放那些被占用的内存,供其他新的对象使用。也就是说,我们不必担心内存回收的问题了。你只需简单创建对象即可。当其不再被需要时,能自行被垃圾收集器释放。垃圾回收机制有效防止了因程序员忘记释放内存而造成的“内存泄漏”问题。
类的创建
class关键词用来标记类,来创建一种对象的类型。
class ATypeName {
// 这里是类的内部
}
可以使用new的方式这种类型的对象。
ATypeName a = new ATypeName();
字段位于类(class)的内部,可以是数值类型也可以是引用类型。
class DataOnly {
int i;
double d;
boolean b;
}
我们可以在创建了对象后使用字段。
DataOnly data = new DataOnly();
data.i = 47;
data.d = 1.1;
data.b = false;
方法表示类型(class)可以做的“行为”。创建一个方法:
[返回类型] [方法名](/*参数列表*/){
// 方法体
}
使用一个方法:
[对象引用].[方法名](参数1, 参数2, 参数3);
当返回类型为 void 时, return 关键字仅用于退出方法,因此在方法结束处的 return 可被省略。我们可以随时从方法中返回,但若方法返回类型为非 void,则编译器会强制我们返回相应类型的值。
命名空间
Java使用反向 URL 的方式来解决不同模块的命名问题。
Java 创建者希望我们反向使用自己的网络域名,因为域名通常是唯一的。因此我的域名是 MindviewInc.com,所以我将我的 foibles 类库命名为 com.mindviewinc.utility.foibles。反转域名后,. 用来代表子目录的划分。
但是这种命名方式导致了一个问题,目录结构可能会因为命名而变得越来越复杂,因为每一个“.”就代表一个目录。而且这种目录的路径也会很长。这时候就需要一个类库的专门管理者,例如像 IntelliJ IDEA这样的IDE。
我们可以使用import关键词使用其他模块的代码。
import java.util.ArrayList;
import java.util.*;//可以使用通配符 * 来导入其中部分类
static关键字
因为只有在用new的方式创建一个类型的对象后,才会分配数据存储空间。这会导致如果我们某些时候想简便的,直接创建一个可以使用的字段或者方法时忽然发现还得先创建对象。所以,Java引入了C++的static关键词。
class StaticTest {
static int i = 47;
}
以下两个StaticTest 对象共享同一个字段i,i只占一份存储空间。
StaticTest st1 = new StaticTest();
StaticTest st2 = new StaticTest();
第一个Java程序
// objects/HelloDate.java
import java.util.*;
public class HelloDate {
public static void main(String[] args) {
System.out.println("Hello, it's: ");
System.out.println(new Date());
}
}
在你的Java环境都安装无误的前提下,移动到子目录 objects 下并键入:
javac HelloDate.java
java HelloDate
其结果输出当前时间。
Java使用“驼峰命名法”。对于类,方法,字段(成员变量)和对象引用名都采用驼峰命名的方式,区别是类的命名首字母需要大写,但是其他的首字母不需要大写。
第四章 运算符
本章总字数:14000
关键词:
- 基础运算符
- 关系运算符
- 类型转换
作者在本章一开始就说明了,如果读者已经了解 C 或 C++的基本运算符,大可以跳过本章和下一章。我的建议也一样。四、五两章适合对基础语法不了解的读者。
基础运算符
几乎所有运算符都只能操作基本类型(Primitives)。唯一的例外是 =、== 和 !=,它们能操作所有对象(这也是令人混淆的一个地方)。除此以外,String 类支持 + 和 +=。
// operators/Precedence.java
public class Precedence {
public static void main(String[] args) {
int x = 1, y = 2, z = 3;
int a = x + y - 2/2 + z; // [1]
int b = x + (y - 2)/(2 + z); // [2]
System.out.println("a = " + a);
System.out.println("b = " + b);
}
}
结果:
a = 5
b = 1
括号改变了运算符的优先级,而优先级的不同会导致计算方式的区别。所以结果也不相同。此外,字符串使用的+号表示将两个字符串拼接。“编译器会将 + 连接的非字符串尝试转换为字符串”。
Java 的基本算术运算符与其他大多编程语言是相同的。其中包括加号 +、减号 -、除号 /、乘号 * 以及取模 %(从整数除法中获得余数)。整数除法会直接砍掉小数,而不是进位。
Java使用+ + 与- - 的方式表示递增与递减。
// operators/AutoInc.java
// 演示 ++ 和 -- 运算符
public class AutoInc {
public static void main(String[] args) {
int i = 1;
System.out.println("i: " + i);
System.out.println("++i: " + ++i); // 前递增
System.out.println("i++: " + i++); // 后递增
System.out.println("i: " + i);
System.out.println("--i: " + --i); // 前递减
System.out.println("i--: " + i--); // 后递减
System.out.println("i: " + i);
}
}
结果:
i: 1
++i: 2
i++: 2
i: 3
--i: 2
i--: 2
i: 1
关系运算符
关系运算符会通过产生一个布尔(boolean)结果来表示操作数之间的关系。如果关系为真,则结果为 true,如果关系为假,则结果为 false。关系运算符包括小于 <,大于 >,小于或等于 <=,大于或等于 >=,等于 == 和不等于 !=。== 和 != 可用于所有基本类型,但其他运算符不能用于基本类型 boolean,因为布尔值只能表示 true 或 false,所以比较它们之间的“大于”或“小于”没有意义。
// operators/Equivalence.java
public class Equivalence {
public static void main(String[] args) {
Integer n1 = 47;
Integer n2 = 47;
System.out.println(n1 == n2);
System.out.println(n1 != n2);
System.out.println(n1.equals(n2));
}
}
结果:
true
false
true
三元运算符
布尔表达式 ? 值 1 : 值 2
//若表达式计算为 true,则返回结果 值 1 ;如果表达式的计算为 false,则返回结果 值 2。
示例:
// operators/TernaryIfElse.java
public class TernaryIfElse {
static int ternary(int i) {
return i < 10 ? i * 100 : i * 10;
}
static int standardIfElse(int i) {
if(i < 10)
return i * 100;
else
return i * 10;
}
public static void main(String[] args) {
System.out.println(ternary(9));
System.out.println(ternary(10));
System.out.println(standardIfElse(9));
System.out.println(standardIfElse(10));
}
}
结果:
900
100
900
100
假若我们打算频繁使用它(三元运算符)的话,还是先多作一些思量: 它易于产生可读性差的代码。
类型转换
在不同类型数据的运算时,会出现类型上的转换。“例如,假设我们为 float 变量赋值一个整数值,计算机会将 int 自动转换成 float。”我们可以使用在数据前加数据类型的方式强制转换数据类型。
// operators/Casting.java
public class Casting {
public static void main(String[] args) {
int i = 200;
long lng = (long)i;
lng = i; // 没有必要的类型提升
long lng2 = (long)200;
lng2 = 200;
// 类型收缩
i = (int)lng2; // Cast required
}
}
诚然,你可以这样地去转换一个数值类型的变量。但是上例这种做法是多余的:因为编译器会在必要时自动提升 int 型数据为 long 型。
当然,为了程序逻辑清晰或提醒自己留意,我们也可以显式地类型转换。
java.lang.Math 的 round() 方法可以对数值进行四舍五入。“因为 round() 方法是 java.lang 的一部分,所以我们无需通过 import 就可以使用。”
第五章 控制流
本章总字数:7600
关键词:
- if-else
- do-while
- for
- for-in
- switch
if-else
如果符合某一个条件,则做某种操作。这种逻辑是常用的。if-else的作用就是如此。
示例:
// control/IfElse.java
public class IfElse {
static int result = 0;
static void test(int testval, int target) {
if(testval > target)
result = +1;
else if(testval < target) // [1]
result = -1;
else
result = 0; // Match
}
public static void main(String[] args) {
test(10, 5);
System.out.println(result);
test(5, 10);
System.out.println(result);
test(5, 5);
System.out.println(result);
}
}
结果:
1
-1
0
其中else 是可选的,表示“例外”的情况。else if 则表示例外当中符合某种条件的情况。
迭代
while ,do-while 和 for 三种语句实现了循环做某种操作的逻辑。
示例:
int i=0;
while(i<=3)
{
i++;
System.out.println("Inside 'while'");
}
结果:
Inside 'while'
Inside 'while'
Inside 'while'
Inside 'while'
while 和 do-while 之间的唯一区别是:即使条件表达式返回结果为 false, do-while 语句也至少会执行一次。
for 循环在第一次迭代之前执行初始化。随后,它会执行布尔表达式,并在每次迭代结束时,进行某种形式的步进。
// control/ListCharacters.java
public class ListCharacters {
public static void main(String[] args) {
for(char c = 0; c < 128; c++)
if(Character.isLowerCase(c))
System.out.println("value: " + (int)c +
" character: " + c);
}
}
value: 97 character: a
value: 98 character: b
value: 99 character: c
...
for-in 是Java5开始引入的写法。是对for的增强使用版,可以用来循环操纵集合与数组。
示例:
// control/ForInString.java
public class ForInString {
public static void main(String[] args) {
for(char c: "An African Swallow".toCharArray())
System.out.print(c + " ");
}
}
结果:
A n A f r i c a n S w a l l o w
break 和 continue
在任何迭代语句的主体内,都可以使用 break 和 continue 来控制循环的流程。 其中,break 表示跳出当前循环体。而 continue 表示停止本次循环,开始下一次循环。
switch
当 if-else语句很多的时候,switch 语句的优势就体现出来了。它可以很清晰的展现逻辑而不用写过多的条件语句。
switch(integral-selector) {
case integral-value1 : statement; break;
case integral-value2 : statement; break;
case integral-value3 : statement; break;
case integral-value4 : statement; break;
case integral-value5 : statement; break;
// ...
default: statement;
}
integral-selector (整数选择因子)是一个能够产生整数值的表达式,switch 能够将这个表达式的结果与每个 integral-value (整数值)相比较。若发现相符的,就执行对应的语句(简单或复合语句,其中并不需要括号)。若没有发现相符的,就执行 default 语句。
在Java7开始,选择因子除了 int 和char ,加入了对字符串的使用。在后续的章节里作者会介绍 switch搭配枚举的使用。
作者:Mr.Jimmy
出处:https://www.cnblogs.com/JHelius
联系:yanyangzhihuo@foxmail.com
如有疑问欢迎讨论,转载请注明出处