151. Java relative

优秀的讲解

类与对象

表述

类描述了一系列具有相同特征(即数据元素)和行为(即功能方法)的对象
一个上佳的方法是将对象想象成“服务提供者” 。你的程序本身也是为用户提供服务的,它通过使用其他对象提供的服务来做到这一点。所以,你的任务是创建(更好的情况是,从已有的库中找到)一些提供对应服务以解决问题的对象。

内存方面

创建一个类即描述了其对象的外观和行为。直到使用 new 关键字时,你才会真正创建一个对象,以及为该对象分配内存,并使得其方法可以被调用。

消息

调用方法

调用方法的行为被描述为 “==向一个对象发送一条消息==”,如:

1
int x = a.f();
  • f() 代表消息

  • a 代表对象

面向对象编程

面向对象编程描述为“==向对象发送消息==”

接口与类的关系

是抽象与更抽象的关系。

比如,定义人这个类,但是在人之外还有各种动物,或者说,人就是高级动物。但是有一个问题是,人有学历之类的属性,而其他动物一定要有学历这一属性吗?答案是否。人有自己的行为动作,那么其他动物是否也有相同或者类似的行为动作呢?答案是是的。那么我们是不是就可以设计一个动物类,而不添加任何属性呢?那么这个类就可以称作接口了。

引用

引用的作用是关联对象。

引用未必会关联某个对象,如:String s

引用的个人理解

引用就是一个地址或者指针,通过地址来操作相关对象

创建对象

new

使用 new 关键字来创建对象: 我要一个这种类型的对象

1
String s = new String9("asdf");

初始化

数组

  • 放置对象的数组:初始化为 null
  • 放置基本类型的数组:初始化为 0

对象

基本类型 默认值
boolean false
char \u0000(null)
byte (byte)0
short (short)0
int 0
long 0L
float 0.0f
double 0.0d
引用 null
调用引用的方法会==得到异常==,但仍然可以打印一个 null 引用而不会抛出异常

初始化顺序

  1. 自动初始化
  2. 构造初始化

静态数据

  • 字段是基本类型:基本类型的标准初始值
  • 字段是对象引用:默认初始值为 null

初始化引用

  1. 在定义对象时。这意味着它们将始终在调用构造器之前被初始化。
  2. 在该类的构造器中。
  3. 在对象实际使用之前。这通常称为延迟初始化(lazy initialization) 。在对象创建成本高昂且不需要每次都创建的情况下,它可以减少开销。
  4. 使用实例初始化

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// reuse/Bath.java
// 使用组合进行构造器初始化
class Soap {
private String s;

Soap() {
System.out.println("Soap()");
s = "Constructed";
}

@Override
public String toString() {
return s;
}
}

public class Bath {
// 在定义时初始化
private String s1 = "Happy", s2 = "Happy", s3, s4;
private Soap castile;
private int i;
private float toy;

public Bath() {
System.out.println("Inside Bath()");
// 构造初始化
s3 = "Joy";
toy = 3.14f;
castile = new Soap();
}

// 实例初始化
{
i = 47;
}

@Override
public String toString() {
// 延迟初始化
if (s4 == null)
s4 = "Joy";
return "s1 = " + s1 + "\n" + "s2 = " + s2 + "\n" + "s3 = " + s3 + "\n" + "s4 = " + s4 + "\n" + "i = " + i + "\n"
+ "toy = " + toy + "\n" + "castile = " + castile;
}

public static void main(String[] args) {
Bath b = new Bath();
System.out.println(b);
}
}

与C不同点

同名变量

1
2
3
4
5
6
{
int x = 12;
{
int x = 96; // 语法错误
}
}

Java 编译器会提示说,变量 x 已经定义过了。

==类似 C 和 C++ 那样在外围的作用域中“隐藏”变量的方式在 Java 中是不被允许的==

  • 文件中必须存在一个与该文件同名的类(如果你没有这么做,则编译器会报错)

入口

如果你需要创建一个能够独立运行的程序,那么与文件同名的类中还必须包含一个程序启动的入口方法。这个特殊的方法叫作 main(),具有以下的格式和返回值:

1
public static void main(String[] args)

编译

  1. javac <filename>.java
  2. java <filename>

赋值

基本类型

基本类型存储了实际的值,而非只想一个对象的引用,所以在为其赋值的时候,你直接将一个地方的==内容复制到了另一个地方==。

对象赋值

当操作一个对象的时候,我们==真正操作的是这个对象的引用==。当“将一个对象赋值给另一个对象”时,你是将这个引用从一个地方复制到了另一个地方。这意味着对对对象而言,将两个变量同时指向同一个对象。

方法调用中的别名

将一个对象作为参数传递给方法时,也会产生别名

格式化处理

符号 含义
%n 换行
%e 表示以科学记数法显示结果

整数值对象

创建方式

  • 自动转化为 Integer

    通过对 Integer.valueOf() 的==自动调用==来完成的

    1
    2
    int value = 0;
    Integer i = value;
  • 使用标准的对象创建语法 new。 这是以前创建 “包装/装箱” Integer 对象的首选方法

    1
    2
    int value = 0;
    Integer r = new Integer(value);
  • 从 Java 9 开始,valueOf() 优于 [2]。如果尝试在 Java 9 中使用方式 [2],你将收到警告,并被建议使用 [3] 代替。很难确定 [3] 是否的确优于 [1],不过 [1] 看起来更简洁

    1
    2
    int value = 0;
    Integer v = Integer.valueOf(value);
  • 基本类型 int 也可以当作整数值对象使用

    1
    int value = 0;

equal()

  • equals() 方法的默认行为是比较引用。
  • 如果只想比较内容,你必须重写 equals() 方法
  • 大多数标准库会重写 equals() 方法来比较对象的内容而不是它们的引用

boolean

  • 把 int 类型当作 boolean 类型并==不合法==
  • 如果在应该使用字符串的地方使用了布尔值,布尔值会自动转换成合适的文本格式
  • boolean 类型是有限制的:
    • 我们只能赋予它 true 和 false 值,并测试它是真还是假
    • 不能将 boolean 值相加,或对 boolean 值执行其他任何运算

字面值

  • 大写(或小写)的字符 L 表示 long 类型(不过使用小写的 l 容易让人迷惑,因为它看起来就像数字 1)

  • 大写(或小写)的字符 F 表示 float 类型

  • 大写(或小写)的字符 D 表示 double 类型

  • Java 7 引入了==二进制字面量==,通过前缀 0b0B 来表示,它适用于所有整数类型。

  • 可以在数字字面量里使用下划线,这样更易于阅读

    如:341_435_936.445_667

    规则:

    • 只能使用单个下划线,不能连续使用多个;
    • 数字的开头或结尾不能有下划线;
    • FDL 这样的后缀周围不能有下划线;
    • 在二进制或十六进制标识符 bx 的周围不能有下划线。
  • e 表示 “10 的幂次”

    e 大小写都可以,含义相同

  • 编译器一般会将指数作为 double 类型处理,所以如果没有尾部的 f, 我们会收到一条出错提示, 告诉我们必须将 double 类型转换成 float 类型

布尔类型位运算

  • 可以对它执行按位“与” 、按位“或”和按位“异或”运算,但==不能执行按位“非”==
  • 对于布尔值,按位操作符和逻辑操作符具有相同的效果,但它们==不会“短路”==
  • 针对布尔值的按位运算还比逻辑操作符多了一个“异或”运算

移位操作

  • 移位操作符也操纵二进制位,它们==只能用来处理基本类型里的整数类型==。
  • 左移位操作符(==<<==)会将操作符左侧的操作数向左移动,移动的位数在操作符右侧指定(==低位补 0==)
  • “有符号”的右移位操作符(==>>==)则按照操作符右侧指定的位数将操作符左侧的操作数向右移动。“有符号”的右移位操作符使用了“符号扩展” :
    • 如果符号为正,则在==高位插入 0==
    • 否则在==高位插入 1==
  • 种“无符号”的右移位操作符(==>>>==) ,它使用“零扩展”: 无论符号为正还是为负,都在==高位插入 0==
  • 无符号”右移位操作符结合赋值操作符可能会遇到一个问题:如果对 byte 或 short 值进行移位运算,得到的可能不是正确的结果。它们会先被提升为 int 类型,进行右移操作,然后在被赋回给原来的变量时==被截断==, 这时得到==结果是 -1==

字符串操作符 + 和 +=

如果表达式以一个字符串开头,则其后的所有操作数都必须是字符串类型的(编译器会自动把双引号里的字符序列转换成字符串)

字符串转换并不取决于先后顺序

类型转换

自动转换

Java 中无法自动将 int 类型转为 boolean 类型

强制转换

既可以对数值进行类型转换, 也可以对变量进行类型转换

窄化转型

有可能面临信息丢失的危险。窄化转型就是说,将能容纳更多信息的数据类型==转换成无法容纳那么多信息的数据类型==。此时,编译器会要求我们进行强制类型转换,意在提醒我们: “这可能是一个危险的操作,如果的确要这么做,你必须显式地进行类型转换。”

宽化转型

不必显式地进行类型转换, 因为新类型可以容纳比原来的类型更多的信息, 而不会造成任何信息的丢失

截尾与舍入

在执行窄化转型时,会出现截尾与舍入

处理

  • float 或 double 转型为整型值时: 截尾

    java.lang.Math 中的 round(): 舍入

提升

  • 如果对小于 int 类型的基本数据类型(即 char、byte 或者 short)执行算术运算或按位运算,运算执行前这些值就会被自动提升为 int,结果也是 int 类型。
  • 如果要把结果赋值给较小的类型,就==必须使用强制类型转换==(由于把值赋给了较小的类型,可能会出现信息丢失) 。
  • 表达式里出现的最大的数据类型决定了表达式最终结果的数据类型

不允许转换

  • boolean

    Java 可以把任何基本类型转换成别的基本类型,但 boolean 除外,它不允许进行任何类型的转换处理。

  • “类”类型(class type)也不允许进行类型转换

    将一种类型转换成另一种类型需要采用特殊的方法:对象可以在它的类型所属族群里进行类型转换,但不能把它转换成外部的类型。

Java 没有 sizeof()

逗号操作符

逗号操作符不是 ==逗号分隔符==

  • 逗号分隔符:分割生命和函数的不同参数

  • 逗号操作符:Java 里==唯一==用到逗号操作符的地方就是 for 循环的控制表达式。

    • 使用地方:初始化和步进部分,这些语句会按先后顺序执行
    • 注意:可以在for语句里定义多个变量,但它们必须是 ==相同的类型==

循环

for-in

用于数组和容器,也叫做 foreach 语法或 for-in 语法

说明

for-in 会==自动为你生成每一项元素==,这样你就不需要创建 int 变量来对这个元素构成的序列进行计数。

使用

任何反悔了数组的方法都可以使用 for-in

range()

range() 方法可以让我们在更多的地方使用 for-in 语法,从而提高了可读性

总结

不仅方便代码的编写,更重要的是让代码更容易阅读,它表明了你打算做什么(获取数组中的每一个元素) , 作者更提倡使用 for-in 语法

无限循环

  • for(;;)
  • while(true)

goto

Java中没有goto

标签

与goto使用了相同的机制

说明

标签是以冒号结尾的标识符

1
label:

用法

在 Java 中,放置标签的==唯一地方==是正好在迭代语句之前。 “正好”的意思就是,==不要在标签和迭代之间插入任何语句==

break 和 continue 通常只会中断当前循环,但和标签一起用时,它们可以中断这个嵌套的循环,直接跳转到标签所在的位置

注意

  • 使用标签的==唯一理由==就是你用到了嵌套循环,而且你需要使用 break 或 continue 来跳出多层的嵌套。

switch

语法

1
2
3
4
5
6
7
8
9
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)是一个能生成整数值的表达式

    • int

    • char

    • 字符串

    • enum

初始化

静态块

Java 允许在一个类里将多个静态初始化语句放在一个特殊的“静态子句”里(有时称为静态块)

1
2
3
4
5
6
public class Spoon {
static int i;
static {
i = 47;
}
}

只执行一次:第一次创建该类的对象时,或第一次访问该类的静态成员时(即使从未创建过该类的对象) 。

实例初始化

Java 提供了一种称为实例初始化(instance initialization)的类似语法,==用于初始化每个对象的非静态变量==。

可以用来保证无论调用哪个显式的构造器,某些操作都会发生

实例初始化子句在==构造器之前执行==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Mugs {
Mug mug1;
Mug mug2;

// 实例初始化
{
mug1 = new Mug(1);
mug2 = new Mug(2);
}

Mugs() {
System.out.println("Mugs()");
}

Mugs(int i) {
System.out.println("Mugs(int)");
}
}

数组

定义

  • 方法1

    1
    int[] a1;
  • 方法2

    1
    int a1[];

注意

  • 编译器不允许指定数组的大小

  • 所拥有的只是对数组的引用(你已经为该引用分配了足够的存储空间) ,并没有为数组对象本身分配任何空间

  • 创建了一个非基本类型数组,其实就是创建了一个引用数组

    即便使用了 new 来创建数组之后,它还只是一个引用数组,直到通过自动装箱为数组里的每个引用本身初始化了一个Integer 对象之后,这个数组的初始化才真正结束

初始化

特殊的初始化方式

只能在创建数组的地方出现,编译器负责存储的分配(相当于使用 new)

1
int[] a1 = {1, 2, 3, 4, 5}

默认

自动初始化为空值(对于数值类型和 char 是 0, 对于 boolean 则是 false)

花括号包围列表

使用花括号保卫列表来初始化对象数组

  • 方法1:局限性更大,它只能用在定义数组的时候

    1
    Integer[] a = {1,  2, 3, };
  • 方法2: 在任何地方使用第二种形式(定义了 b) ,甚至在方法调用中也可以

    1
    Integer[] b = new Integer[]{ 1,  2, 3,  };

注意

  • 初始值列表的最后一个逗号都是可选的(此功能可以让维护长列表更容易)

运算

赋值

可以将一个数组赋值给另一个数组

1
a2 = a1

其实真正做的知识复制了一个引用

查询数组有多少个元素

成员变量: length

可变参数列表

如果列表中没有任何内容,则它会转变成一个大小为零的数组

函数

printBinaryInt() 和 printBinaryLong()

分别接受 int 类型和 long 类型的参数,然后输出其二进制格式

%n

Java 用 %n 来实现的功能,它会根据程序运行的平台生成合适的换行符,不过这仅会在使用 System.out.printf()System.out.format() 时起作用。对于 System.out.println(),你仍然必须使用 \n;如果使用了 %n, println() 只会输出 %n 而不是将其当作换行符。

toBinaryString()

使用 Integer 和 Long 类的静态方法 toBinaryString() 可以很容易实现获取二进制

如果将比较小的类型传递给Integer.toBinaryString() 方法,则该类型会自动被转换为 int 类型

Integer.toString()

1
2
// Integer.toString() 的简化版:
System.out.println("" + x);

Array.toString(a)

1
2
int[] a = new int[10];
System.out.println(Array.toString(a));

getClass()

生成一个对象的类,当打印这个类时,你会看到一个表示该类类型的编码字符串

[ 表示这是后面紧随的类型的数组

ordinal()

表示特定 enum 常量的声明顺序

一个静态的values() 方法

它按照声明顺序生成一个 enum 常量值的数组

自动类型推断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// housekeeping/TypeInference.java 
// {NewFeature} 从 JDK 11 开始
class Plumbus {
}

public class TypeInference {
void method() {
// 显式类型:
String hello1 = "Hello";
// 类型推断:
var hello = "Hello!";
// 用户定义的类型也起作用:
Plumbus pb1 = new Plumbus();
var pb2 = new Plumbus();
}

// 静态方法里也可以启用:
static void staticMethod() {
var hello = "Hello!";
var pb2 = new Plumbus();
}
}

class NoInference {
String field1 = "Field initialization";
// var field2 = "Can't do this";

// void method() {
// var noInitializer; // No inference data
// var aNull = null; // No inference data //
// }

// var inferReturnType() {
// return "Can't infer return type"; //
// }
}

static

static 修饰的字段或方法(也称“类数据 class data” 和 “类方法class method”),只服务于类,而非特定的对象。这些数据或方法只会占用同一块内存空间,即会被由该类创建所有的对象所共享。

注意

调用 static 方法并不依赖于是否提前创建对象。正因为如此,我们也==不能在没有具体对象的情况下, 使用 static 方法直接调用非 static 成员或方法==(因为非 static 成员和方法必须基于特定的对象才能 运行) 。

调用方法

  • 通过对象调用: <对象>.<static 成员>

  • 通过类名调用: <类名>.<static 成员> (==推荐使用该方法==)

    注意:==非 static 成员则不能这样使用==

生成方式

  • static 字段

    • 是基于类创建的
    • 即便你没有创建对象,也可以调用该方法
  • 非 static 字段

    • 是基于对象创建的

见下

访问控制

  • 每个编译单元(文件)都只能有一个public类,但可以根据需要拥有任意数量的包访问权限的类
    • public 类的名称必须与包含编译单元的文件名完全匹配,包括大小写
    • 编译单元里可以没有 public 类。这时你可以随意命名文件
  • 类访问权限, 只有两种选择: 包访问权限和 public
    • 若防止对该类的访问,可以将其所有的构造器都设为 private

Math

函数 功能
random() 生成一个范围为0~1(包括 0,但不包括 1)的 double 值

java.lang.Character 包装器类

函数 功能
isLowerCase() 检测出相关字符是否为小写字母

问题

包与库的关系

不清楚

toString()

每个非基本类型的对象都有一个 toString() 方法

它会在一些特殊情况下被调用,比如当编译器需要一个字符串,但有的是一个对象时。

注解

注解 @Override 用在 toString() 方法上

@Override 是可选的

final

final 数据

  • 定义常量

空白 final

final 执行赋值的操作只能发生在两个地方

  • 在字段定义处使用表达式进行赋值
  • 在每个构造器中

final 参数

在方法内部不能更改参数引用指向的内容

可以读取这个参数, 但不能修改它

final 方法

原因

  • 在方法上放置一个“锁” ,这样就可以防止继承类通过重写来改变该方法的含义
  • 效率:编译器可以将任何对该方法的调用转换为内联调用 (现在可以不用考虑这个因素了,主要考虑 “锁” 的因素)

final 和 private

类中的任何 private 方法都是隐式的 final

final 类

将整个类定义为 final 时(通过在其定义前加上 final 关键字) ,就阻止了该类的所有继承