01.对象无所不在

01 对象无所不在

虽然源于C++,Java却是一门更为“纯正”的面向对象语言。

Java和C++都是混合型语言,混合型编程语言允许采用多种编程风格。

Java语言预期只编写面向对象程序。在本章,你将学习构建Java程序的基础组件,并认识Java中(几乎)一切都是对象这个事实。

1、通过引用操作对象

  1. 实际操作的是引用,再由引用来修改对象信息
  2. 引用可以不关联对象独立存在

所有编程语言都在内存中处理各种元素。

String s;

这样只是创建了一个引用而非对象。此时向对象s发送消息,则会得到一条报错信息NullPointException,因为此时s还没有连接到任何对象。

一种安全做法:始终在创建对象时进行初始化。

String s = "asdf";

此处使用了Java的一个特性:字符串(String)可以用带引号的文本进行初始化。而对于其它类型的对象而言,需要使用 new 进行初始化。(更为通用)

2、必须创建所有对象

  1. 引用的作用是关联对象
  2. 通常我们使用new关键字来创建对象

new关键字表达的意思是“我要一个这种类型的对象”。

String s = new String("asdf");

这一行代码不仅代表了“创建一个新的字符串”,同时也告诉我们如何如何通过一组字符来创建String对象。

2.1 数据保存在哪里

  1. 寄存器(register):这是速度最快的数据存储方式,因为它保存数据的位置不同与其它方式:数据会直接保存在*中央处理器(CPU)*里。然而寄存器的数量有限,所以只能按需分配。此外,我们不能直接控制寄存器的分配,甚至在程序中都找不到寄存器存在过的证据(C 和 C++除外)
  2. 栈(stack):数据存储在随机存储器(random-access memory,RAM)里,处理器可以通过栈指针(stack pointer)直接操作该数据。具体来说,栈指针向下移动将申请一块新的内存,向上移动则会释放这块内存。这是一种极其迅速和高效的内存分配方式,其效率仅次于寄存器。只不过Java系统在创建应用程序时就必须明确栈上所有对象的生命周期。这种限制约束了程序的灵活性,因此虽然有一些数据会保存在栈上(尤其是对象引用),对象本身并非如此。
  3. 堆(heap): 这是一个通用的内存池(使用的也是RAM空间),用于存放所有Java对象。与栈不同,编译器并不关心堆上的对象需要存在多久。因此,堆的使用是非常灵活的。当你需要一个对象时,可以随时在堆上为该对象分配内存空间。然而这种灵活性是有代价的:分配和清理堆存储要比栈存储花费更多的时间。好消息是:随着时间的推移,Java的堆内存分配机制已经变得非常高效了,所以你并不需要太过关注此类问题。
  4. 常量存储(constant storage):常量通常会直接保存在程序代码中(方法区)中,因为它们的值不会改变,所以这样做是安全的。有时候常量会与其它代码隔离开来,于是在某些嵌入系统里,这些常量就可以保存在只读存储器(read-only memory,ROM)中。
  5. 非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),通过包装类可以将基本类型呈现为位于堆上的非原始对象。例如:

  1. char c = 'x';	
    Character ch = new Character(c);
    
  2. Character ch = new Character('x');
    

“自动装箱”机制(autoboxing)能够将基本类型对象自动转换为包装类对象,例如:

  1. Character ch = 'x';
    
  2. 也可以在转换回来:自动拆箱

    char c = ch;
    

2.3 高精度数字

Java中提供了两个支持高精度计算的类:精度换速度

  1. BigInteger:支持任意精度的整数,在运算时可以表示任意精度的整数值,而不用担心丢失精度。
  2. BigDecimal:可用于任意精度的定点数。例如,可以将其用于货币计算。

详细看附录2:02.附录:Java中Math、BigInteger、BigDecimal详解

2.4 Java数组

Java的一个核心设计目的是安全,于是Java解决了C和C++使用数组的许多问题:

  1. Java的数组一定会被初始化
  2. 无法访问Java数组边界之外的元素

这种边界检查的代价是需要消耗少许内存,以及运行时需要少量时间来验证索引的正确性。其背后的假设是:安全性和生产力的改善完全可以抵消这些代价,Java同时也会优化这些操作。

  1. 创建一个对象数组,数组里存放的实际上是对象引用,这些引用会被自动初始化为特殊值 null。Java会认为一个值为null的引用没有指向任何对象,操作引用前需要确保其指向了某个对象。否则 NullPointException。
  2. 创建一个基本类型数组,编译器会确保该数据进行初始化,并将数组元素值设为 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 关键字的作用:

  1. 告诉我们“从方法中离开吧,一切都结束了”。
  2. 当该方法生成了一个值的时候,将这个值放置于 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 关键字时,才会真正创建一个对象,以及为该对象分配内存,并使得其方法可以被调用。

然而在两种情况下,这种方式会显得不合时宜:

  1. 有时候我们需要一小块共享空间来保存某个特定的字段,而不关心创建多少个对象,甚至有没有创建对象
  2. 需要使用一个类的某个方法,而该方法和具体的对象无关;换句话说,希望即便没有生成该类的对象,依然可以调用其方法

static 关键字(源自 C++)为上面两个问题提供了解决方案。

如果使用 static 关键字,则意味着使用 static 的字段或方法不依赖于任何对象。也就是说,即便没有为一个类创建任何对象,依旧可以调用该类的 static 方法或 static 字段。另外,由于非 static 的字段和方法必须基于指定的对象,因此对于非 static 的字段和方法来说,必须创建一个对象才可以使用非 static 的字段和方法。

创建一个 static 字段或方法,只需要把 static 关键字放置于字段或方法定义的前面即可:

class StaticTest {
    static int i = 47;
}

即便创建了两个 StaticTest 类的对象,StaticTest.i 依然只会占用同一块内存空间。即,字段 i 会被 StaticTest 的所有实例对象所共享。

有两种方法可以调用 static 变量/方法:

  1. 通过对象调用
  2. 通过类名调用(非 static 成员不能这样使用)——推荐使用,突出了变量的 static 特质

将 static 关键字应用于字段,毫无疑问会改变其数据的生成方式,即 static 字段是基于类创建的,非 static 字段则是基于对象创建的。而将 static 应用于方法时,即便没有创建对象,也可以通过类名调用该方法。后面会看到,作为程序应用的入口,main 方法中 static 关键字是必不可少的一部分。

8、你的第一个 Java 程序

在每一个程序文件的起始处,你都必须使用 import 语句将所有额外的类导入到文件中。这里说 “额外” 是因为,所有 Java 文件都会自动导入一个特定的类库:java.lang

java.lang.System 类拥有几个字段,其中一个字段 out 是一个 static PrintStream 对象。因为 outstatic 修饰的,所以无须借助 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 编程的基础运算符,并展示如何控制程序流程。

你可能感兴趣的:(OnJava笔记,开发语言,java)