感谢该项目的开源从中学到了很多的知识,对于java也有了更深的了解
已经看了两遍,里面写的很好,从中收获了跟多,也认识到了自己的许多不足。对于其中的知识还是有很多的不解,希望自己慢慢的进步。值得在看几遍,肯定会收获更多的知识
第三章 万物皆对象
数据存储
那么,程序在运行时是如何存储的呢?尤其是内存是怎么分配的。有5个不同的地方可以存储数据:
- 寄存器(Registers)最快的存储区域,位于 CPU 内部 0。然而,寄存器的数量十分有限,所以寄存器根据需求进行分配。我们对其没有直接的控制权,也无法在自己的程序里找到寄存器存在的踪迹(另一方面,C/C++ 允许开发者向编译器建议寄存器的分配)。
- 栈内存(Stack)存在于常规内存 RAM(随机访问存储器,Random Access Memory)区域中,可通过栈指针获得处理器的直接支持。栈指针下移分配内存,上移释放内存,这是一种快速有效的内存分配方法,速度仅次于寄存器。创建程序时,Java 系统必须准确地知道栈内保存的所有项的生命周期。这种约束限制了程序的灵活性。因此,虽然在栈内存上存在一些 Java 数据,特别是对象引用,但 Java 对象却是保存在堆内存的。
- 堆内存(Heap)这是一种通用的内存池(也在 RAM 区域),所有 Java 对象都存在于其中。与栈内存不同,编译器不需要知道对象必须在堆内存上停留多长时间。因此,用堆内存保存数据更具灵活性。创建一个对象时,只需用
new
命令实例化对象即可,当执行代码时,会自动在堆中进行内存分配。这种灵活性是有代价的:分配和清理堆内存要比栈内存需要更多的时间(如果可以用 Java 在栈内存上创建对象,就像在 C++ 中那样的话)。随着时间的推移,Java 的堆内存分配机制现在已经非常快,因此这不是一个值得关心的问题了。 - 常量存储(Constant storage)常量值通常直接放在程序代码中,因为它们永远不会改变。如需严格保护,可考虑将它们置于只读存储器 ROM (只读存储器,Read Only Memory)中 0。
- 非 RAM 存储(Non-RAM storage)数据完全存在于程序之外,在程序未运行以及脱离程序控制后依然存在。两个主要的例子:(1)序列化对象:对象被转换为字节流,通常被发送到另一台机器;(2)持久化对象:对象被放置在磁盘上,即使程序终止,数据依然存在。这些存储的方式都是将对象转存于另一个介质中,并在需要时恢复成常规的、基于 RAM 的对象。Java 为轻量级持久化提供了支持。而诸如 JDBC 和 Hibernate 这些类库为使用数据库存储和检索对象信息提供了更复杂的支持。
基本类型的存储
有一组类型在 Java 中使用频率很高,它们需要特殊对待,这就是 Java 的基本类型。之所以这么说,是因为它们的创建并不是通过 new
关键字来产生。通常 new
出来的对象都是保存在堆内存中的,以此方式创建小而简单的变量往往是不划算的。所以对于这些基本类型的创建方法,Java 使用了和 C/C++ 一样的策略。也就是说,不是使用 new
创建变量,而是使用一个“自动”变量。 这个变量直接存储”值”,并置于栈内存中,因此更加高效。
Java 确定了每种基本类型的内存占用大小。 这些大小不会像其他一些语言那样随着机器环境的变化而变化。这种不变性也是 Java 更具可移植性的一个原因。
为什么Java中int型数据取值范围是[-2^{31}, 2^{31}-1]
1Byte=8bit(一个字节占8位)
基本类型 | 大小 | 最小值 | 最大值 | 包装类型 |
---|---|---|---|---|
boolean | — | — | — | Boolean |
char | 16 bits/2Byte | Unicode 0 | Unicode 216 -1 | Character |
byte | 8 bits/1Byte | -2^7 | +2^7-1 | Byte |
short | 16 bits/2Byte | - 2^15 | + 2^15 -1 | Short |
int | 32 bits/4Byte | - 2^31 | + 2^31 -1 | Integer |
long | 64 bits/8Byte | - 2^63 | + 2^63 -1 | Long |
float | 32 bits/4Byte | IEEE754 | IEEE754 | Float |
double | 64 bits/8Byte | IEEE754 | IEEE754 | Double |
void | — | — | — | Void |
基本类型默认值
如果类的成员变量(字段)是基本类型,那么在类初始化时,这些类型将会被赋予一个初始值。
基本类型 | 初始值 |
---|---|
boolean | false |
char | \u0000 (null) |
byte | (byte) 0 |
short | (short) 0 |
int | 0 |
long | 0L |
float | 0.0f |
double | 0.0d |
这些默认值仅在 Java 初始化类的时候才会被赋予。这种方式确保了基本类型的字段始终能被初始化(在 C++ 中不会),从而减少了 bug 的来源。但是,这些初始值对于程序来说并不一定是合法或者正确的。 所以,为了安全,我们最好始终显式地初始化变量。【局部变量一定要赋初值,全局变量不一定要赋初值】
对于 Long 型数值,结尾使用大写 L
或小写 l
皆可(不推荐使用 l
,因为容易与阿拉伯数值 1 混淆)。大写 F
或小写 f
表示 float 浮点数。大写 D
或小写 d
表示 double 双精度。
下划线
Java 7 中有一个深思熟虑的补充:我们可以在数字字面量中包含下划线 _
,以使结果更清晰。这对于大数值的分组特别有用。代码示例:
// operators/Underscores.java
public class Underscores {
public static void main(String[] args) {
double d = 341_435_936.445_667;
System.out.println(d);
int bin = 0b0010_1111_1010_1111_1010_1111_1010_1111;
System.out.println(Integer.toBinaryString(bin));
System.out.printf("%x%n", bin); // [1]
long hex = 0x7f_e9_b7_aa;
System.out.printf("%x%n", hex);
}
}复制ErrorOK!
输出结果:
3.41435936445667E8
101111101011111010111110101111
2fafafaf
7fe9b7aa复制ErrorOK!
下面是合理使用的规则:
- 仅限单
_
,不能多条相连。 - 数值开头和结尾不允许出现
_
。 F
、D
和L
的前后禁止出现_
。- 二进制前导
b
和 十六进制x
前后禁止出现_
。
[1] 注意 %n
的使用。熟悉 C 风格的程序员可能习惯于看到 \n
来表示换行符。问题在于它给你的是一个“Unix风格”的换行符。此外,如果我们使用的是 Windows,则必须指定 \r\n
。这种差异的包袱应该由编程语言来解决。这就是 Java 用 %n
实现的可以忽略平台间差异而生成适当的换行符,但只有当你使用 System.out.printf()
或 System.out.format()
时。对于 System.out.println()
,我们仍然必须使用 \n
;如果你使用 %n
,println()
只会输出 %n
而不是换行符。
对象作用域
Java 对象与基本类型具有不同的生命周期。当我们使用 new
关键字来创建 Java 对象时,它的生命周期将会超出作用域。因此,下面这段代码示例:
{
String s = new String("a string");
}
// 作用域终点复制ErrorOK!
上例中,引用 s 在作用域终点就结束了。但是,引用 s 指向的字符串对象依然还在占用内存。在这段代码中,我们无法在这个作用域之后访问这个对象,因为唯一对它的引用 s 已超出了作用域的范围。在后面的章节中,我们还会学习怎么在编程中传递和复制对象的引用。
只要你需要,new
出来的对象就会一直存活下去。 相比在 C++ 编码中操作内存可能会出现的诸多问题,这些困扰在 Java 中都不复存在了。在 C++ 中你不仅要确保对象的内存在你操作的范围内存在,还必须在使用完它们之后,将其销毁。
那么问题来了:我们在 Java 中并没有主动清理这些对象,那么它是如何避免 C++ 中出现的内存被填满从而阻塞程序的问题呢?答案是:Java 的垃圾收集器会检查所有 new
出来的对象并判断哪些不再可达,继而释放那些被占用的内存,供其他新的对象使用。也就是说,我们不必担心内存回收的问题了。你只需简单创建对象即可。当其不再被需要时,能自行被垃圾收集器释放。垃圾回收机制有效防止了因程序员忘记释放内存而造成的“内存泄漏”问题。
==
和 !=
比较的是对象引用,
那么怎么比较两个对象的内容是否相同呢?你必须使用所有对象(不包括基本类型)中都存在的 equals()
方法,下面是如何使用 equals()
方法的示例:
Math 库的静态方法 random()
。该方法的作用是产生 0 和 1 之间 (包括 0,但不包括 1) 的一个 double 值。
第六章 初始化和清理
在 Java 中,类的设计者通过构造器保证每个对象的初始化。如果一个类有构造器,那么 Java 会在用户使用对象之前(即对象刚创建完成)自动调用对象的构造器方法,从而保证初始化。
返回值的重载
经常会有人困惑,”为什么只能通过类名和参数列表,不能通过方法的返回值区分方法呢?“。例如以下两个方法,它们有相同的命名和参数,但是很容易区分:
void f(){}
int f() {return 1;}复制ErrorOK!
有些情况下,编译器很容易就可以从上下文准确推断出该调用哪个方法,如 int x = f()
。
但是,你可以调用一个方法且忽略返回值。这叫做调用一个函数的副作用,因为你不在乎返回值,只是想利用方法做些事。所以如果你直接调用 f()
,Java 编译器就不知道你想调用哪个方法,阅读者也不明所以。因为这个原因,所以你不能根据返回值类型区分重载的方法。为了支持新特性,Java 8 在一些具体情形下提高了猜测的准确度,但是通常来说并不起作用。
假设现在在方法内部,你想获得对当前对象的引用。但是,对象引用是被秘密地传达给编译器——并不在参数列表中。方便的是,有一个关键字: this 。this 关键字只能在非静态方法内部使用。当你调用一个对象的方法时,this 生成了一个对象引用。你可以像对待其他引用一样对待这个引用。如果你在一个类的方法里调用其他该类中的方法,不要使用 this,直接调用即可,this 自动地应用于其他方法上了。
在构造器中调用构造器
public class Flower {
int petalCount = 0;
String s = "initial value";
Flower(int petals) {
petalCount = petals;
System.out.println("Constructor w/ int arg only, petalCount = " + petalCount);
}
Flower(String ss) {
System.out.println("Constructor w/ string arg only, s = " + ss);
s = ss;
}
Flower(String s, int petals) {
this(petals);
//- this(s); // Can't call two!
this.s = s; // Another use of "this"
System.out.println("String & int args");
}
Flower() {
this("hi", 47);
System.out.println("no-arg constructor");
}
void printPetalCount() {
//- this(11); // Not inside constructor!
System.out.println("petalCount = " + petalCount + " s = " + s);
}
public static void main(String[] args) {
Flower x = new Flower();
x.printPetalCount();
}
}
从构造器 Flower(String s, int petals)
可以看出,其中只能通过 this 调用一次构造器。另外,必须首先调用构造器,否则编译器会报错。这个例子同样展示了 this 的另一个用法。参数列表中的变量名 s 和成员变量名 s 相同,会引起混淆。你可以通过 this.s
表明你指的是成员变量 s,从而避免重复。你经常会在 Java 代码中看到这种用法,同时本书中也会多次出现这种写法。在 printPetalCount()
方法中,编译器不允许你在一个构造器之外的方法里调用构造器。
static 的含义
记住了 this 关键字的内容,你会对 static 修饰的方法有更加深入的理解:static 方法中不会存在 this。你不能在静态方法中调用非静态方法(反之可以)。静态方法是为类而创建的,不需要任何对象。事实上,这就是静态方法的主要目的,静态方法看起来就像全局方法一样,但是 Java 中不允许全局方法,一个类中的静态方法可以被其他的静态方法和静态属性访问。一些人认为静态方法不是面向对象的,因为它们的确具有全局方法的语义。使用静态方法,因为不存在 this,所以你没有向一个对象发送消息。的确,如果你发现代码中出现了大量的 static 方法,就该重新考虑自己的设计了。然而,static 的概念很实用,许多时候都要用到它。至于它是否真的”面向对象”,就留给理论家去讨论吧。
垃圾回收器
类的成员变量会再程序初始时赋一个初始值,而方法的局部变量就不会被赋初值
多态
Java中除静态和final方法(private方法也是隐式的final)外,其他所有方法都是后期绑定。这意味着通常情况下,我们不需要判断后续绑定是否会发生-它自动发生。
private方法只可以在类的内部使用,在类外根本访问不到, 而final方法可以在类外访问,但是不可以重写该方法,就是说可以使用该方法的功能但是不可以改变其功能,这就是private方法和final方法的最大区别
为什么将一个对象指明为 final ?正如前一章所述,它可以防止方法被重写。但更重要的一点可能是,它有效地”关闭了“动态绑定,或者说告诉编译器不需要对其进行动态绑定。这可以让编译器为 final 方法生成更高效的代码。然而,大部分情况下这样做不会对程序的整体性能带来什么改变,因此最好是为了设计使用 final,而不是为了提升性能而使用。
Shape s = new Circle();
这会创建一个 Circle 对象,引用被赋值给 Shape 类型的变量 s,这看似错误(将一种类型赋值给另一种类型),然而是没问题的,因此从继承上可认为圆(Circle)就是一个形状(Shape)。因此编译器认可了赋值语句,没有报错。
假设你调用了一个基类方法(在各个派生类中都被重写):
s.draw()复制ErrorOK!
你可能再次认为 Shape 的 draw()
方法被调用,因为 s 是一个 Shape 引用——编译器怎么可能知道要做其他的事呢?然而,由于后期绑定(多态)被调用的是 Circle 的 draw()
方法,这是正确的。
private 方法可以当作是 final 的,结论是只有非私有方法才能被重建,但得小心重新改写私有方法的现象,编译器不报错,但不会按我们所预期的执行。为了清晰起见,派生类中的方法名采用与基类中私有方法名不同的命名。
如果一个方法是静态(static)的,它的行为就不具有多态性:字类重写了父类的static方法,子类向上转型时,昂对新调用的方法是父类的静态方法。静态的方法只与类关联,与个别的对象无关。(静态方法会和类一起放到方法区中)
在派生类的构造过程中总会调用基类的构造器。初始化会自动按继承层次结构上移,因此每个基类的构造器都会被调用到。这样做是可行的,因为构造器存在特殊的任务:检查对象是否被正确地构造。由于属性通常声明为私有,您必须假定派生类只能访问自己的成员而不能访问基类的成员。只有基类的构造器拥有适当的知识和权限。来初始化自身的元素。因此,必须获得调用所有构造器;否则就不能构造完整的对象。这就是编译器强制每个派生类部分必须调用构造器的原因。如果没有无参构造器,编译器就会报错(当类中排除构造器时,编译器会自动合成一个无参构造器)。
磨对象的构造器调用顺序如下:
- 基类构造器被调用。这个步骤被递归归零重复,这样一来类层次的顶级父类会被最先构造,然后是它的派生类,以此类推,直到最容易的派生类。
- 按声明顺序初始化成员。
- 称为派生类构造器的方法体。
在构造器中必须确保所有的成员都已经完成。唯一能保证这点的方法就是首先调用基类的构造器。然后,在派生类的构造器中,所有你可以访问的基类成员都已经已经初始化。另一个在构造器中能知道所有成员都是有效的理由是:无论何时有可能的话,你应该在所有成员对象(通过组合将对象放置类中)定义处初始化它们(例如,示例中的b,c和l)。如果遵循这条实践,就可以帮助确保所有的基类成员和当前对象的成员对象都已经初始化。
销毁的顺序应该与初始化的顺序相反,以防一个对象依赖另一个对象。对于属性来说,就这是因为因为基类(遵循C ++析构函数的形式),首先进行派生类的清理工作,然后才是基类的清理。派生类的清理可能调用基类的一些方法,所以基类组件这时得存活,不能过早地被销毁。输出显示了,Frog对象的所有部分都是按照创建的逆序销毁的。
字类继承了父类,并重写了父类中的某一个方法,父类在构造函数中调用了该方法,在子类初始化时,父类构造其中调用的方法是字类重写过的方法。在对象初始化前,先对类中的变量进行初始化(0或与0等价的变量)
初始化的实际过程是:
- 在所有事发生前,分配给对象的存储空间会被初始化为二进制0。
- 如前所述调用基类构造器。此时调用重写后的
draw()
方法(是的,在调用RoundGraph构造器之前调用),由步骤1可知,radius的值为0。 - 按声明顺序初始化成员。
- 最终调用派生类的构造器。
因此,编写构造器有一条良好的规范:做尽量少的事让对象进入良好状态。如果有可能的话,尽量不要调用类中的任何方法。在基类的构造器中能安全调用的只有基类的最后的方法(这也适用于可被看作是最后的私有方法)。这些方法不能被重写,因此不会产生意想不到的结果。你可能无法永远遵循这条规范,但应该朝着它努力。
Java 5中约会了协变返回类型,这表示派生类的被重写方法可以返回基类方法返回类型的派生类型:
学习过多态之后,一切看似都可以被继承,因为多态有助于简化的工具。这会给设计带来负担。事实上,如果利用现有类创建新类首先选择继承的话,事情会变得莫名的复杂。
更好的方法是首先选择组合,特别是不知道该使用哪种方法时。组合不会强制设计是继承层次结构,而且组合更加灵活,因为可以动态地选择类型(从而选择相应的行为),而继承要求必须在编译时知道初始化类型。
由于向上转换(在继承层次中向上移动)会丢失特定的类型信息,那么为了重新获取类型信息,就需要在继承层次中向下移动,使用*向下转换*。
向上转换永远是安全的,因为基基类不会具有比派生类更多的接口。因此,每条发送给基类接口的消息都能被接收。但是对于向下转换,你无法知道一个形状是圆,它有可能是三角形,正方形或其他一些类型。
在某些语言中(如C ++),必须执行一个特殊的操作来获得安全的向下转换,但是在Java中,每次转换都会被检查!所以只是进行一次普通的加括号形式的类型转换,在运行时这个转换仍会被检查,以确保它的确是希望的那种类型。如果不是,就会得到ClassCastException(类转换异常)。这种在运行时检查类型的行为可以运行时类型信息。
第十章接口
我们将学习抽象类,一种介于普通类和接口之间的折中手段。虽然你的第一想法是创建接口,但对于具有属性和未实现方法的类来说,抽象类也是重要且必要的工具。您不可能总是使用纯粹的接口。
创建一个抽象类是为了通过通用接口操纵一系列类
抽象类中不一定有抽象方法,抽象方法一定在抽象类中
Java提供了一个叫做*抽象方法*的机制,这个方法是不完整的:它只有声明没有方法体。
abstract void f();
包含抽象方法的类称为*抽象类*。如果一个类包含一个或多个抽象方法,那么类本身也必须限定为抽象的,否则,编译器会报错。
如果创建一个继承抽象类的新类并为之创建对象,那么就必须为基类的所有抽象方法提供方法定义。如果不这么做(可以选择不做),新类仍然是一个抽象类,编译器会强制我们为新类加上 abstract 关键字。
事实上,接口只允许public方法,如果不加访问修饰符的话,接口的方法不是friendly。而是public。所以当实现一个接口时,来自接口中的方法必须被定义为公共。
private abstract 被禁止了是有意义的,因为你不可能在 AbstractAccess 的任何子类中合法地定义它。
然而,抽象类允许每件事:
// interfaces/AbstractAccess.java
abstract class AbstractAccess {
private void m1() {}
// private abstract void m1a(); // illegal
protected void m2() {}
protected abstract void m2a();
void m3() {}
abstract void m3a();
public void m4() {}
public abstract void m4a();
}复制错误OK!
private abstract被禁止了是有价值的,因为你不可能在AbstractAccess的任何子类中合法地定义它。
将一个类说明为抽象并不强制类中的所有方法必须都是抽象方法。
- 抽象类不一定有抽象方法,有抽象方法的类一定是抽象类
接口中的方法是默认的修饰符是public abstract(也就是抽象方法)
Java 8 允许在接口中添加静态方法。
default
如果我们使用关键字default为newMethod()
方法提供替代的实现,那么所有与接口有关的代码能正常工作,不应对,而且这些代码还可以调用新的方法newMethod()
:
// interfaces/InterfaceWithDefault.java
interface InterfaceWithDefault {
void firstMethod();
void secondMethod();
default void newMethod() {
System.out.println("newMethod");
}
}
关键字default允许在接口中提供方法实现-在Java 8之前被禁止。
- 可以在接口中实现方法,只不过要加上关键字default,这样实现类即使不实现该方法,实现类的对象仍然可以使用该方法
java可以实现多实现
class MI implements One, Two, Three {}
方法签名包括方法名称和参数类型,返回类型不是方法签名的一部分。
实现的多个接口中如果有方法签名相同的方法,可以使用super指定方法为某个接口中的默认实现(default)
public void f1() {
Three.super.f1();
}
当然,你可以重定义f1()
方法,但是也能像上例中那样使用super关键字选择基类实现中的一种。
Java 8允许在接口中添加静态方法。这样做能适当地把工具功能插入接口中,从而操作接口,或者成为通用的工具:
public interface Operations {
void execute();
static void runOps(Operations... ops) {
for (Operations op: ops) {
op.execute();
}
}
static void show(String msg) {
System.out.println(msg);
}
}
这是模版方法设计模式的一个版本(在“设计模式”一章中详细描述),runOps()
是一个模版方法。runOps()
使用可变参数列表,因此我们可以选择任意多的操作参数并按顺序运行它们:
接口中也可以定义变量
接口的成员特点: A:成员变量 只能是常量。默认修饰符 public static final B:成员方法 只能是抽象方法。默认修饰符 public abstract
interface Instrument {
// Compile-time constant:
int VALUE = 5; // static & final public static final
default void play(Note n) // Automatically public
System.out.println(this + ".play() " + n);
}
default void adjust() {
System.out.println("Adjusting " + this);
}
void eat(); //注意:要给出初始值 public abstract
}
抽象类和接口
特性 | 接口 | 抽象类 |
---|---|---|
组合 | 新类可以组合多个接口 | 只能继承单一抽象类 |
状态 | 不能包含属性(除了静态属性,不支持对象状态) | 可以包含属性,非抽象方法可能引用这些属性 |
默认方法和抽象方法 | 不需要在子类中实现替代方法。替代方法可以引用其他接口的方法 | 必须在子类中实现抽象方法 |
构造器 | 没有构造器 | 可以有构造器 |
可见性 | 隐式public | 可以是受保护的或友元 |
抽象类仍然是一个类,在创建新类时只能继承它一个。而创建类的过程中可以实现多个接口。
有一次实际经验:可以进行地抽象。因此,更高级使用接口而不是抽象类。只有当必要时才使用抽象类。除非必须使用,否则不要用接口和抽象类。大多数时候,普通类已经做得很好,如果不行的话,再移动到接口或抽象类中。
通过这种方式结合具体类和接口时,需要将具体类放在前面,后面跟着接口(否则编译器会报错)。
同时继承父类和实现接口时,需要将继承父类写在前面,实现接口写在后面
class Hero extends ActionCharacter implements CanFight, CanSwim, CanFly {}
interface I3 {
int f();
}
class C {
public int f() {
return 1;
}
}
class C4 extends C implements I3 {
// 完全相同,没问题
@Override
public int f() {
return 1;
}
}
关于Random r = new Random(47)中47的意思
https://blog.csdn.net/zhang41228/article/details/77069734
https://www.cnblogs.com/java-zy/p/8204512.html
通过Math.random()来获取随机数。实际上,它返回的是0(包含)到1(不包含)【0,1)之间的double值。使用方法如下:
final double d = Math.random();
第十一章内部类
当生成一个内部类的对象时,此对象与制造它的外围对象(包围对象)之间就有了一种联系,所以它能访问其外围对象的所有成员,而不需要任何特殊条件。从而,内部类还拥有其外围类的所有元素的访问权。
内部类的对象只能在相关外围类的对象相关联的情况下才能被创造(就像你应该看到的,内部类是非静态类时)。内置内部类对象时,需要一个指向其外围类对象的引用,如果编译器访问不到这个引用就会报错。不过有时时候这都无需程序员操心。
如果您需要生成对外部类对象的引用,可以使用外部类的名字后面紧跟圆点和this。任何运行时预算。以下的示例展示了如何使用.this:
// innerclasses/DotThis.java
// Accessing the outer-class object
public class DotThis {
void f() { System.out.println("DotThis.f()"); }
public class Inner {
public DotThis outer() {
return DotThis.this;
// A plain "this" would be Inner's "this"
}
}
public Inner inner() { return new Inner(); }
public static void main(String[] args) {
DotThis dt = new DotThis();
DotThis.Inner dti = dt.inner();
dti.outer().f();
}
}
要想直接创建内部类的对象,你不能按照自己想象的方式,去引用外部类的名字DotNew,或者必须使用外部类的对象来创建该内部类对象,就像在上面的程序中所看到的的那样。这也解决了内部类名称作用域的问题,因此你不必声明(实际上你不能声明)dn.new DotNew.Inner。
你必须在新表达式中提供对其他外部类对象的引用,这是需要使用.new语法,就像下面这样:
// innerclasses/DotNew.java
// Creating an inner class directly using .new syntax
public class DotNew {
public class Inner {}
public static void main(String[] args) {
DotNew dn = new DotNew();
DotNew.Inner dni = dn.new Inner();
}
}
如果定义一个匿名内部类,并且希望它使用一个在其外部定义的对象,那么编译器会要求其参数引用是final的(即,它在初始化后不会改变,因此可以被视为final) ,就像你在destination()
的参数中看到的那样。这里省略掉final也没问题,但是通常最好加上final作为一种暗示。
在匿名类中定义定义时,还能够执行初始化操作:
// innerclasses/Parcel9.java
public class Parcel9 {
// Argument must be final or "effectively final"
// to use within the anonymous inner class:
public Destination destination(final String dest) {
return new Destination() {
private String label = dest;
@Override
public String readLabel() { return label; }
};
}
public static void main(String[] args) {
Parcel9 p = new Parcel9();
Destination d = p.destination("Tasmania");
}
}
下例是带实例初始化的“包裹”形式。注意destination()
的参数必须是final的,因为它们是在匿名类内部使用的(译者注:即使不加final,Java 8的编译器也会为我们自动加上final,以保证数据的一致性)。
// innerclasses/Parcel10.java
// Using "instance initialization" to perform
// construction on an anonymous inner class
public class Parcel10 {
public Destination
destination(final String dest, final float price) {
return new Destination() {
private int cost;
// Instance initialization for each object:
{
cost = Math.round(price);
if(cost > 100)
System.out.println("Over budget!");
}
private String label = dest;
@Override
public String readLabel() { return label; }
};
}
public static void main(String[] args) {
Parcel10 p = new Parcel10();
Destination d = p.destination("Tasmania", 101.395F);
}
}
嵌套类
如果不需要内部类对象与其外围类对象之间有联系,那么可以将内部类声明为静态的,这通常称为嵌套类。想要理解静态应用于内部类时的含义,就必须记住,普通的内部类对象隐式地保存了一个引用,指向创建它的外围类对象。而,当内部类是静态的时,就不是这样了。
- 要创建嵌套类的对象,并且不需要其外围类的对象。
- 不能从嵌套类的对象中访问非静态的外围类对象。
普通内部类的细分与方法,只能放在类的外部基础上,所以普通的内部类不能有静态数据和静态分支,也不能包含嵌套类。 。但是嵌入类可以包含所有这些东西:
// innerclasses/Parcel11.java
// Nested classes (static inner classes)
public class Parcel11 {
private static class ParcelContents implements Contents {
private int i = 11;
@Override
public int value() { return i; }
}
protected static final class ParcelDestination
implements Destination {
private String label;
private ParcelDestination(String whereTo) {
label = whereTo;
}
@Override
public String readLabel() { return label; }
// Nested classes can contain other static elements:
public static void f() {}
static int x = 10;
static class AnotherLevel {
public static void f() {}
static int x = 10;
}
}
public static Destination destination(String s) {
return new ParcelDestination(s);
}
public static Contents contents() {
return new ParcelContents();
}
public static void main(String[] args) {
Contents c = contents();
Destination d = destination("Tasmania");
}
}
在main()
中,没有任何Parcel11的对象是必需的;或者使用选择静态成员的普通语法来调用方法-这些方法返回对Contents和Destination的引用。
就像你在本章前面看到的那样,在一个普通的(非静态)内部类中,通过一个特殊的此引用可以链接到其外围类对象。嵌套类就没有这个特殊的此引用,这使得它是一个静态方法。
接口内部的类
public interface ClassInInterface {
void howdy();
class Test implements ClassInInterface {
@Override
public void howdy() {
System.out.println("Howdy!");
}
public static void main(String[] args) {
new Test().howdy();
}
}
}
一般说来,内部类继承自某个类或实现某个接口,内部类的代码操作创建它的外围类的对象。所以可以认为内部类提供了某种进入其外围类的窗口。
内部类必须要回答的一个问题是:如果只是需要一个对接口的引用,为什么不通过外围类实现那个接口呢?答案是:“如果这能满足需求,那么就应该这样做。”那么内部类实现一个接口与外围类实现此接口有什么区别呢?答案是:另外不是总能吸收到接口带来的方便,有时需要用到接口的实现。所以,使用内部类最吸引人的原因是:
每个内部类都能独立地继承自一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响。
如果没有内部类提供的,可以继承多个特定的或抽象的类的能力,一些设计与编程问题就很难解决。从这个角度看,内部类可以多重继承的解决方案变得完整。接口解决了部分,而内部类有效地实现了“多重继承”。进而,内部类允许继承多个非接口类型(译注:类或抽象类)。
如果拥有的是抽象的类或具体的类,而不是接口,那么只能使用内部类才能实现多重继承:
java8提供了闭包,没有提供指针的概念
内部类也可以被继承
如果创建了一个内部类,然后继承其外围类并重新定义此内部类时,会发生什么呢?从而,内部类可以被覆盖吗?这看起来是个很有用的思想,但是“覆盖”内部类就好像它是外围类的一个方法,其实并不起什么作用:
内部类是延时加载的,也就是说只会在第一次使用时加载。不使用就不加载,所以可以很好的实现单例模式。
java构造函数的执行时机
- 先执行内部静态对象的构造方法,如果有多个按定义的先后顺序执行;静态对象在构造的时候也是也先执行其内部的静态对象。
- 再调用父类的构造方法(父类还有父类的话,从最开始的基类开始调用),如果没有明显指定调用父类自定义的构造方法,那么编译器会调用默认的父类构造方法super()。但是如果要调用父类自定义的构造方法,要在子类的构造方法中明确指定。
- 按声明顺序将成员引用对象变量初始化。
- 最后调用自身的构造方法。
局部内部类
前面提到过,可以在代码块里创建内部类,典型的方式是在一个方法体的里面创建。局部内部类不能有访问说明符,因为它不是外围类的一部分;但是它可以访问内部代码块内的常量,以及此外围类的所有成员。下面的示例对局部内部类与匿名内部类的创建进行了比较。
第十二章集合
java.util库提供了一套相当完整的*集合类*(集合类)来解决这个问题,其中基本的类型有List,Set,Queue和Map。这些类型也被*容器类*(container classes)
可以把ArrayList命名为“可以自动扩充自身尺寸的数组”来看待使用。ArrayList的相当简单:创建一个实例,用add()
插入对象;然后用get()
。来访问这些对象,此时需要使用索引,就像数组那样,但是不需要方括号0ArrayList还有一个size()
方法,来说明集合中包含了多少个元素,所以不会不小心因数组越界而引发错误(通过抛出*运行时异常*,异常章节介绍了异常)。
看到可以new ArrayList<>()
这有时被称为“菱形语法”(菱形语法)在Java 7中之前,必须要在两端都进行类型声明,如下所示。:
ArrayList<Apple> apples = new ArrayList<Apple>();
之后不需要在右侧声明
ArrayList<Apple> apples = new ArrayList<>();
使用泛型,从列表中获取元素不需要强制类型转换。因为清单知道它持有什么类型,当因此调用get()
时,它会替你执行转型。因此,使用泛型,你不仅知道编译器将检查放入集合的对象类型,而且在使用集合中的对象时也可以获得更清晰的语法。
当指定了某个类型为泛型参数时,并同时只能将重定向类型的对象放入集合中。向上转换也可以像作用于其他类型一样作用于泛型:
GrannySmith@15db9742
程序的输出是从对象默认的toString()
方法产生的,该方法打印类名,后边跟着对象的散列码的无符号十六进制表示(散列这个码的英文通过hashCode()
方法产生的)
- 集合(集合):一个独立元素的序列,这些元素都服从一条或多条规则列表必须以插入的顺序保存元素,设置不能包含重复元素,队列按照*排队规则*来确定对象产生的顺序(通常与它们被插入的顺序相同)。
映射(MAP) :一组成对的“键值对”对象,允许使用键来查找值的ArrayList。使用数字来查找对象,因此在某种意义上讲,它是将数字和对象关联在一起 映射允许我们使用一个对象来查找另一个对象,它也被细分*关联*数组(associative array),因为指向对象和其他对象关联在一起;或者称为*字典*(dictionary),因为可以使用一个键对象来查找值对象,就像在字典中使用单词查找定义一样。Map是强大的编程工具。
List<Apple> apples = new ArrayList<>();复制错误OK!
请注意,ArrayList已经被向上转换为List,这与之前示例中的处理方式正好相反。使用接口的目的是,如果想要改变具体实现,只需在创建时修改它就行了,就像下面这样:
在java.util包中的数组和集合类中都有很多实用的方法,可以在一个集合中添加一组元素。
Arrays.asList()
方法接受一个包含一个逗号或逗号分隔的元素列表(使用可变参数),将其转换为列表对象。Collections.addAll()
方法接受一个Collection对象,以及一个或另一个逗号分隔的列表,将其中元素添加到Collection中。下边的示例展示了这两个方法,以及更通用的addAll()
方法,所有收藏类型都包含该方法:
public class AddingGroups {
public static void main(String[] args) {
Collection<Integer> collection =
new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
Integer[] moreInts = { 6, 7, 8, 9, 10 };
collection.addAll(Arrays.asList(moreInts));
// Runs significantly faster, but you can't
// construct a Collection this way:
Collections.addAll(collection, 11, 12, 13, 14, 15);
Collections.addAll(collection, moreInts);
// Produces a list "backed by" an array:
List<Integer> list = Arrays.asList(16,17,18,19,20);
list.set(1, 99); // OK -- modify an element
// list.add(21); // Runtime error; the underlying
// array cannot be resized.
}
}
使用必须Arrays.toString()
来生成数组的柯林斯打印形式但是打印集合无需任何帮助下面的英文一个例子,这个例子中也介绍了基本的Java的集合。:
public class PrintingCollections {
static Collection
fill(Collection<String> collection) {
collection.add("rat");
collection.add("cat");
collection.add("dog");
collection.add("dog");
return collection;
}
static Map fill(Map<String, String> map) {
map.put("rat", "Fuzzy");
map.put("cat", "Rags");
map.put("dog", "Bosco");
map.put("dog", "Spot");
return map;
}
public static void main(String[] args) {
System.out.println(fill(new ArrayList<>()));
System.out.println(fill(new LinkedList<>()));
System.out.println(fill(new HashSet<>()));
System.out.println(fill(new TreeSet<>()));
System.out.println(fill(new LinkedHashSet<>()));
System.out.println(fill(new HashMap<>()));
System.out.println(fill(new TreeMap<>()));
System.out.println(fill(new LinkedHashMap<>()));
}
}
/* Output:
[rat, cat, dog, dog]
[rat, cat, dog, dog]
[rat, cat, dog]
[cat, dog, rat]
[rat, cat, dog]
{rat=Fuzzy, cat=Rags, dog=Spot}
{cat=Rags, dog=Spot, rat=Fuzzy}
{rat=Fuzzy, cat=Rags, dog=Spot}
*/
ArrayList和LinkedList都是List的类型,从输出中可以修剪,它们都按插入顺序保存元素。两者之间的区别在于执行某些类型的操作时的性能,而且LinkedList包含操作的多于ArrayList。
HashSet的,TreeSet中和LinkedHashSet是设置的类型。从输出中可以看到,Set仅保存每个相同项中的一个,并且不同的Set实现存储元素的方式也不同。HashSet的使用相当复杂的方法存储元素。这种技术是检索元素的最快方法,因此,存储顺序看上去没有什么意义(通常只关心某事物是否是集合的成员,而存储顺序并不重要。。如果存储顺序很重要,则可以使用TreeSet,则按按比较结果的升序保存对象)或LinkedHashSet,它按被添加的先后顺序保存对象。
Map(称为也。*关联数组*)使用*键*来查找对象,就像一个简单的数据库。关联所的对象称为*值*。假设有一个Map将美国州名与它们的首府联系在一起,如果想要俄亥俄州(Ohio)的首府,可以使用“ Ohio”作为键来查找,几乎就像使用数组下标一样。正是由于这种行为,对于每个键,地图只存储了一次。
Map.put(key, value)
添加一个所要添加的值插入它与一个键(找到查找值)相关联。Map.get(key)
生成与该键相关联的值。上面的示例仅添加键值对,并没有执行查找。这将在稍稍后展示。
请注意,这里没有指定(或考虑)Map的大小,因为它会自动调整大小。此外,Map还知道如何打印自己,它会显示相关联的键和值。
本例使用了Map的三种基本风格:HashMap,TreeMap和LinkedHashMap。
键和值保存在HashMap中中的顺序不是插入顺序,因为HashMap的实现使用了非常快速的算法来控制顺序。TreeMap中通过比较结果的升序来保存键,LinkedHashMap的在保持HashMap的查找速度的同时按键的插入顺序保存键。
有两种类型的List:
- 基本的ArrayList,擅长长访问元素,但在List中间插入和删除元素时速度较慢。
- LinkedList的,它通过代价较低的在List中间进行的插入和删除操作,提供了优化的顺序访问。LinkedList的对于随机访问来说相对较慢,但它具有比ArrayList的更大的特征集。
下面的示例引入typeinfo.pets,超前使用了类型信息一章中的类库。这个类库包含了Pet类层次结构,并且使用了随机生成的Pet对象的一些工具类。此时不需要了解完整的详细信息,只需要知道两点:
- 有一个宠物类,以及Pet的各种子类型。
- 静态的
Pets.arrayList()
方法返回一个填充了随机选取的宠物对象的ArrayList:
对于LinkedList,在列表中间插入和删除都是廉价操作(在本例中,除了对列表中间进行的真正的随机访问),但对于ArrayList,这可是代价高昂的操作。这是否意味着永远不应该在ArrayList的中间插入元素,并最好是转换为LinkedList?不,它只是意味着你应该认识这个问题,如果你开始在某个ArrayList中间执行很多插入操作,并且程序开始变慢,那么你应该看看你的列表实现有可能就是罪魁祸首(发现这种情况的最佳方式)是一个使用分析器profiler)。优化是一个很棘手的问题,最好的策略就是置之不顾,直到发现必须要去担心它了(尽管去理解这些问题总是一个很好的主意)。
因此,查找通常是Set最重要的操作,因此通常会选择HashSet实现,该实现针对快速查找进行了优化。
Set,与Collection 相同的接口,因此没有任何额外的功能,不像前面两种不同类型的List那样。实际上,Set就是一个Collection ,只是行为不同。(这是继承和多态思想的典型应用:表现不同的行为)。Set根据对象的“值”确定归属性,
字符串对象似乎没有排序要对结果进行排序,一种方法是使用。TreeSet中而不是HashSet的:
TreeSet可以对字符串进行排序
通过使用containsKey()
和containsValue()
方法去测试一个Map,以查看它是否包含某个键或某个值:
LinkedList实现了队列接口,并提供了一些方法以支持行为,因此LinkedList可以利用队列的一种实现。通过将LinkedList向上转换为Queue
Java 提供了许多保存对象的方法:
- 数组将数字索引与对象相关联。它保存类型明确的对象,因此在查找对象时不必对结果做类型转换。它可以是多维的,可以保存基本类型的数据。虽然可以在运行时创建数组,但是一旦创建数组,就无法更改数组的大小。
- Collection 保存单一的元素,而 Map 包含相关联的键值对。使用 Java 泛型,可以指定集合中保存的对象的类型,因此不能将错误类型的对象放入集合中,并且在从集合中获取元素时,不必进行类型转换。各种 Collection 和各种 Map 都可以在你向其中添加更多的元素时,自动调整其尺寸大小。集合不能保存基本类型,但自动装箱机制会负责执行基本类型和集合中保存的包装类型之间的双向转换。
- 像数组一样, List 也将数字索引与对象相关联,因此,数组和 List 都是有序集合。
- 如果要执行大量的随机访问,则使用 ArrayList ,如果要经常从表中间插入或删除元素,则应该使用 LinkedList 。
- 队列和堆栈的行为是通过 LinkedList 提供的。
- Map 是一种将对象(而非数字)与对象相关联的设计。 HashMap 专为快速访问而设计,而 TreeMap 保持键始终处于排序状态,所以没有 HashMap 快。 LinkedHashMap 按插入顺序保存其元素,但使用散列提供快速访问的能力。
- Set 不接受重复元素。 HashSet 提供最快的查询速度,而 TreeSet 保持元素处于排序状态。 LinkedHashSet 按插入顺序保存其元素,但使用散列提供快速访问的能力。
- 不要在新代码中使用遗留类 Vector ,Hashtable 和 Stack 。
JDK5以后引入了forin语句,目的是为了简化迭代器遍历,其本质仍然是迭代器遍历。
如果一个对象想使用forin语句进行遍历,则对象类必须满足两个条件:实现Iterable
接口和实现Iterator
方法。之所以ArrayList
集合类能够实现forin语句遍历,就是因为其满足上述两个条件
第十三章函数式编程
尽管Java不是函数式语言,但Java 8 Lambda表达式和方法引用(方法参考)允许您以函数式编程。
OO(面向对象,面向对象)是抽象数据,FP(功能编程,函数式编程)是抽象行为。
Lambda表达式
Lambda 表达式是使用最小可能语法编写的函数定义:
- Lambda 表达式产生函数,而不是类。 在 JVM(Java Virtual Machine,Java 虚拟机)上,一切都是一个类,因此在幕后执行各种操作使 Lambda 看起来像函数 —— 但作为程序员,你可以高兴地假装它们“只是函数”。
- Lambda 语法尽可能少,这正是为了使 Lambda 易于编写和使用。
我们在 Strategize.java 中看到了一个 Lambda 表达式,但还有其他语法变体:
// functional/LambdaExpressions.java
interface Description {
String brief();
}
interface Body {
String detailed(String head);
}
interface Multi {
String twoArg(String head, Double d);
}
public class LambdaExpressions {
static Body bod = h -> h + " No Parens!"; // [1]
static Body bod2 = (h) -> h + " More details"; // [2]
static Description desc = () -> "Short info"; // [3]
static Multi mult = (h, n) -> h + n; // [4]
static Description moreLines = () -> { // [5]
System.out.println("moreLines()");
return "from moreLines()";
};
public static void main(String[] args) {
System.out.println(bod.detailed("Oh!"));
System.out.println(bod2.detailed("Hi!"));
System.out.println(desc.brief());
System.out.println(mult.twoArg("Pi! ", 3.14159));
System.out.println(moreLines.brief());
}
}复制ErrorOK!
输出结果:
Oh! No Parens!
Hi! More details
Short info
Pi! 3.14159
moreLines()
from moreLines()复制ErrorOK!
我们从三个接口开始,每个接口都有一个单独的方法(很快就会理解它的重要性)。但是,每个方法都有不同数量的参数,以便演示 Lambda 表达式语法。
任何 Lambda 表达式的基本语法是:
- 参数。
- 接着
->
,可视为“产出”。 ->
之后的内容都是方法体。- [1] 当只用一个参数,可以不需要括号
()
。 然而,这是一个特例。 - [2] 正常情况使用括号
()
包裹参数。 为了保持一致性,也可以使用括号()
包裹单个参数,虽然这种情况并不常见。 - [3] 如果没有参数,则必须使用括号
()
表示空参数列表。 - [4] 对于多个参数,将参数列表放在括号
()
中。
- [1] 当只用一个参数,可以不需要括号
到目前为止,所有 Lambda 表达式方法体都是单行。 该表达式的结果自动成为 Lambda 表达式的返回值,在此处使用 return 关键字是非法的。 这是 Lambda 表达式缩写用于描述功能的语法的另一种方式。
[5] 如果在 Lambda 表达式中确实需要多行,则必须将这些行放在花括号中。 在这种情况下,就需要使用 return。
Lambda 表达式通常比匿名内部类产生更易读的代码,因此我们将在本书中尽可能使用它们。
递归函数是一个自我调用的函数。可以编写递归的 Lambda 表达式,但需要注意:递归方法必须是实例变量或静态变量,否则会出现编译时错误。 我们将为每个案例创建一个示例。
这两个示例都需要一个接受 int 型参数并生成 int 的接口:
// functional/IntCall.java
interface IntCall {
int call(int arg);
}复制ErrorOK!
整数 n 的阶乘将所有小于或等于 n 的正整数相乘。 阶乘函数是一个常见的递归示例:
// functional/RecursiveFactorial.java
public class RecursiveFactorial {
static IntCall fact;
public static void main(String[] args) {
fact = n -> n == 0 ? 1 : n * fact.call(n - 1);
for(int i = 0; i <= 10; i++)
System.out.println(fact.call(i));
}
}复制ErrorOK!
输出结果:
1
1
2
6
24
120
720
5040
40320
362880
3628800复制ErrorOK!
这里,fact
是一个静态变量。 注意使用三元 if-else。 递归函数将一直调用自己,直到 i == 0
。所有递归函数都有“停止条件”,否则将无限递归并产生异常。
static修饰的方法中不可以调用非static修饰的成员变量,方法中也不可以定义static修饰的局部变量
自学
lambda:最大的作用是简化代码
方法引用
Java 8 方法引用没有历史包袱。方法引用组成:类名或对象名,后面跟 ::
[^4],然后跟方法名称。
方法签名相同
// functional/MethodReferences.java
import java.util.*;
interface Callable { // [1]
void call(String s);
}
class Describe {
void show(String msg) { // [2]
System.out.println(msg);
}
}
public class MethodReferences {
static void hello(String name) { // [3]
System.out.println("Hello, " + name);
}
static class Description {
String about;
Description(String desc) { about = desc; }
void help(String msg) { // [4]
System.out.println(about + " " + msg);
}
}
static class Helper {
static void assist(String msg) { // [5]
System.out.println(msg);
}
}
public static void main(String[] args) {
Describe d = new Describe();
Callable c = d::show; // [6]
c.call("call()"); // [7]
c = MethodReferences::hello; // [8]
c.call("Bob");
c = new Description("valuable")::help; // [9]
c.call("information");
c = Helper::assist; // [10]
c.call("Help!");
}
}复制ErrorOK!
输出结果:
call()
Hello, Bob
valuable information
Help!复制ErrorOK!
[1] 我们从单一方法接口开始(同样,你很快就会了解到这一点的重要性)。
[2] show()
的签名(参数类型和返回类型)符合 Callable 的 call()
的签名。
[3] hello()
也符合 call()
的签名。
[4] help()
也符合,它是静态内部类中的非静态方法。
[5] assist()
是静态内部类中的静态方法。
[6] 我们将 Describe 对象的方法引用赋值给 Callable ,它没有 show()
方法,而是 call()
方法。 但是,Java 似乎接受用这个看似奇怪的赋值,因为方法引用符合 Callable 的 call()
方法的签名。
[7] 我们现在可以通过调用 call()
来调用 show()
,因为 Java 将 call()
映射到 show()
。
[8] 这是一个静态方法引用。
[9] 这是 [6] 的另一个版本:对已实例化对象的方法的引用,有时称为*绑定方法引用*。
[10] 最后,获取静态内部类的方法引用的操作与 [8] 中外部类方式一样。
上例只是简短的介绍,我们很快就能看到方法引用的全部变化。
Runnable接口
Runnable 接口自 1.0 版以来一直在 Java 中,因此不需要导入。它也符合特殊的单方法接口格式:它的方法 run()
不带参数,也没有返回值。因此,我们可以使用 Lambda 表达式和方法引用作为 Runnable:
// functional/RunnableMethodReference.java
// 方法引用与 Runnable 接口的结合使用
class Go {
static void go() {
System.out.println("Go::go()");
}
}
public class RunnableMethodReference {
public static void main(String[] args) {
new Thread(new Runnable() {
public void run() {
System.out.println("Anonymous");
}
}).start();
new Thread(
() -> System.out.println("lambda")
).start();
new Thread(Go::go).start();
}
}复制ErrorOK!
输出结果:
Anonymous
lambda
Go::go()复制ErrorOK!
Thread 对象将 Runnable 作为其构造函数参数,并具有会调用 run()
的方法 start()
。 注意,只有匿名内部类才需要具有名为 run()
的方法。
使用未绑定的引用时,函数方法的签名(接口中的单个方法)不再与方法引用的签名完全匹配。 理由是:你需要一个对象来调用方法。
Java 8 引入了 java.util.function
包。它包含一组接口,这些接口是 Lambda 表达式和方法引用的目标类型。 每个接口只包含一个抽象方法,称为函数式方法。
在编写接口时,可以使用 @FunctionalInterface
注解强制执行此“函数式方法”模式:
Java格式
下表描述了 java.util.function
中的目标类型(包括例外情况):
特征 | 函数式方法名 | 示例 |
---|---|---|
无参数; 无返回值 | Runnable (java.lang) run() |
Runnable |
无参数; 返回类型任意 | Supplier get() getAs类型() |
Supplier`` BooleanSupplier IntSupplier LongSupplier DoubleSupplier |
无参数; 返回类型任意 | Callable (java.util.concurrent) call() |
Callable`` |
1 参数; 无返回值 | Consumer accept() |
Consumer IntConsumer LongConsumer DoubleConsumer |
2 参数 Consumer | BiConsumer accept() |
BiConsumer |
2 参数 Consumer; 1 引用; 1 基本类型 | Obj类型Consumer accept() |
ObjIntConsumer ObjLongConsumer ObjDoubleConsumer |
1 参数; 返回类型不同 | Function apply() To类型 和 类型To类型 applyAs类型() |
FunctionIntFunction LongFunction DoubleFunctionToIntFunction ToLongFunction ToDoubleFunction IntToLongFunction IntToDoubleFunction LongToIntFunction LongToDoubleFunction DoubleToIntFunction DoubleToLongFunction |
1 参数; 返回类型相同 | UnaryOperator apply() |
UnaryOperator IntUnaryOperator LongUnaryOperator DoubleUnaryOperator |
2 参数类型相同; 返回类型相同 | BinaryOperator apply() |
BinaryOperator IntBinaryOperator LongBinaryOperator DoubleBinaryOperator |
2 参数类型相同; 返回整型 | Comparator (java.util) compare() |
Comparator |
2 参数; 返回布尔型 | Predicate test() |
Predicate BiPredicate IntPredicate LongPredicate DoublePredicate |
参数基本类型; 返回基本类型 | 类型To类型Function applyAs类型() |
IntToLongFunction IntToDoubleFunction LongToIntFunction LongToDoubleFunction DoubleToIntFunction DoubleToLongFunction |
2 参数类型不同 | Bi操作 (不同方法名) | BiFunction BiConsumer BiPredicate ToIntBiFunction ToLongBiFunction ToDoubleBiFunction |
java格式化输出https://www.cnblogs.com/Dhouse/p/7776780.html
转 换 符 | 说 明 | 示 例 |
---|---|---|
%s | 字符串类型 | “mingrisoft” |
%c | 字符类型 | ’m’ |
%b | 布尔类型 | true |
%d | 整数类型(十进制) | 99 |
%x | 整数类型(十六进制) | FF |
%o | 整数类型(八进制) | 77 |
%f | 浮点类型 | 99.99 |
%a | 十六进制浮点类型 | FF.35AE |
%e | 指数类型 | 9.38e+5 |
%g | 通用浮点类型(f和e类型中较短的) | |
%h | 散列码 | |
%% | 百分比类型 | % |
%n | 换行符 | |
%tx | 日期与时间类型(x代表不同的日期与时间转换符 |
标 志 | 说 明 | 示 例 | 结 果 |
---|---|---|---|
+ | 为正数或者负数添加符号 | (“%+d”,15) | +15 |
− | 左对齐 | (“%-5d”,15) | |15 | |
0 | 数字前面补0 | (“%04d”, 99) | 0099 |
空格 | 在整数之前添加指定数量的空格 | (“% 4d”, 99) | | 99| |
, | 以“,”对数字分组 | (“%,f”, 9999.99) | 9,999.990000 |
( | 使用括号包含负数 | (“%(f”, -99.99) | (99.990000) |
# | 如果是浮点数则包含小数点,如果是16进制或8进制则添加0x或0 | (“%#x”, 99)(“%#o”, 99) | 0x630143 |
< | 格式化前一个转换符所描述的参数 | (“%f和%<3.2f”, 99.45) | 99.450000和99.45 |
$ | 被格式化的参数索引 | (“%1$d,%2$s”, 99,“abc”) | 99,abc |
常见日期和时间组合的格式,如图所示。
转 换 符 | 说 明 | 示 例 |
---|---|---|
c | 包括全部日期和时间信息 | 星期六 十月 27 14:21:20 CST 2007 |
F | “年-月-日”格式 | 2007-10-27 |
D | “月/日/年”格式 | 10/27/07 |
r | “HH:MM:SS PM”格式(12时制) | 02:25:51 下午 |
T | “HH:MM:SS”格式(24时制) | 14:28:16 |
R | “HH:MM”格式(24时制) | 14:28 |
从 Lambda 表达式引用的局部变量必须是 final
或者是等同 final
效果的。
这就叫做等同 final 效果(Effectively Final)。这个术语是在 Java 8 才开始出现的,表示虽然没有明确地声明变量是 final
的,但是因变量值没被改变过而实际有了 final
同等的效果。 如果局部变量的初始值永远不会改变,那么它实际上就是 final
的。
实际上只要有内部类,就会有闭包(Java 8 只是简化了闭包操作)。在 Java 8 之前,变量 x
和 i
必须被明确声明为 final
。在 Java 8 中,内部类的规则放宽,包括等同 final 效果。
第十四章流式编程
peek
操作接收的是一个 Consumer
函数。顾名思义 peek 操作会按照 Consumer
函数提供的逻辑去消费流中的每一个元素,同时有可能改变元素内部的一些属性。这里我们要提一下这个 Consumer
以理解 什么是消费。
集合优化了对象的存储,而流和对象的处理有关。
流的一个核心好处是,它使得程序更加短小并且更易理解。当 Lambda 表达式和方法引用(method references)和流一起使用的时候会让人感觉自成一体。流使得 Java 8 更具吸引力。
举个例子,假如你要随机展示 5 至 20 之间不重复的整数并进行排序。实际上,你的关注点首先是创建一个有序集合。围绕这个集合进行后续的操作。但是使用流式编程,你就可以简单陈述你想做什么:
// streams/Randoms.java
import java.util.*;
public class Randoms {
public static void main(String[] args) {
new Random(47)
.ints(5, 20)
.distinct()
.limit(7)
.sorted()
.forEach(System.out::println);
}
}
流式编程采用内部迭代,这是流式编程的核心特性之一。这种机制使得编写的代码可读性更强,也更能利用多核处理器的优势。通过放弃对迭代过程的控制,我们把控制权交给并行化机制。
流是懒加载的。这代表着它只在绝对必要时才计算。你可以将流看作“延迟列表”。由于计算延迟,流使我们能够表示非常大(甚至无限)的序列,而不需要考虑内存问题。
:在接口中添加被 default
(默认
)修饰的方法。通过这种方案,设计者们可以将流式(*stream*)方法平滑地嵌入到现有类中。流方法预置的操作几乎已满足了我们平常所有的需求。流操作的类型有三种:创建流,修改流元素(中间操作, Intermediate Operations),消费流元素(终端操作, Terminal Operations)。最后一种类型通常意味着收集流元素(通常是到集合中)。
Java 8 采用的解决方案是:在接口中添加被 default
(默认
)修饰的方法。通过这种方案,设计者们可以将流式(*stream*)方法平滑地嵌入到现有类中。流方法预置的操作几乎已满足了我们平常所有的需求。流操作的类型有三种:创建流,修改流元素(中间操作, Intermediate Operations),消费流元素(终端操作, Terminal Operations)。最后一种类型通常意味着收集流元素(通常是到集合中)。
随机数流
Random
类被一组生成流的方法增强了。代码示例:
// streams/RandomGenerators.java
import java.util.*;
import java.util.stream.*;
public class RandomGenerators {
public static <T> void show(Stream<T> stream) {
stream
.limit(4)
.forEach(System.out::println);
System.out.println("++++++++");
}
public static void main(String[] args) {
Random rand = new Random(47);
show(rand.ints().boxed());
show(rand.longs().boxed());
show(rand.doubles().boxed());
// 控制上限和下限:
show(rand.ints(10, 20).boxed());
show(rand.longs(50, 100).boxed());
show(rand.doubles(20, 30).boxed());
// 控制流大小:
show(rand.ints(2).boxed());
show(rand.longs(2).boxed());
show(rand.doubles(2).boxed());
// 控制流的大小和界限
show(rand.ints(3, 3, 9).boxed());
show(rand.longs(3, 12, 22).boxed());
show(rand.doubles(3, 11.5, 12.3).boxed());
}
}
为了消除冗余代码,我创建了一个泛型方法 show(Stream stream)
(在讲解泛型之前就使用这个特性,确实有点作弊,但是回报是值得的)。类型参数 T
可以是任何类型,所以这个方法对 Integer、Long 和 Double 类型都生效。但是 Random 类只能生成基本类型 int, long, double 的流。幸运的是, boxed()
流操作将会自动地把基本类型包装成为对应的装箱类型,从而使得 show()
能够接受流。
流的创建
你可以通过 Stream.of()
很容易地将一组元素转化成为流
流的中间操作
流的中间操作时惰性的,也就是说没有终段操作流的中间操作时不会执行的
流的终端操作
一个流只能有一个终端操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。终端操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect(副作用)。
第十五章 异常
Java 的基本理念是“结构不佳的代码不能运行”
当抛出异常后,有几件事会随之发生。首先,同 Java 中其他对象的创建一样,将使用 new 在堆上创建异常对象。然后,当前的执行路径(它不能继续下去了)被终止,并且从当前环境中弹出对异常对象的引用。此时,异常处理机制接管程序,并开始寻找一个恰当的地方来继续执行程序。这个恰当的地方就是异常处理程序,它的任务是将程序从错误状态中恢复,以使程序能要么换一种方式运行,要么继续运行下去。
所有标准异常类都有两个构造器:一个是无参构造器;另一个是接受字符串作为参数,以便能把相关信息放入异常对象的构造器:
多重捕获
public class MultiCatch {
void x() throws Except1, Except2, Except3, Except4 {}
void process() {}
void f() {
try {
x();
} catch(Except1 | Except2 | Except3 | Except4 e) {
process();
}
}
}
栈轨迹
try {
throw new Exception();
} catch(Exception e) {
for(StackTraceElement ste : e.getStackTrace())
System.out.println(ste.getMethodName());
}
在 Java 7 之前,try 总是后面跟着一个 {,但是现在可以跟一个带括号的定义 - 这里是我们创建的 FileInputStream 对象。括号内的部分称为资源规范头(resource specification header)。现在可用于整个 try 块的其余部分。更重要的是,无论你如何退出 try 块(正常或异常),都会执行前一个 finally 子句的等价物,但不会编写那些杂乱而棘手的代码。这是一项重要的改进。
它是如何工作的?在 try-with-resources 定义子句中创建的对象(在括号内)必须实现 java.lang.AutoCloseable 接口,这个接口有一个方法:close()。当在 Java 7 中引入 AutoCloseable 时,许多接口和类被修改以实现它;查看 Javadocs 中的 AutoCloseable,可以找到所有实现该接口的类列表,其中包括 Stream 对象:
// exceptions/StreamsAreAutoCloseable.java
import java.io.*;
import java.nio.file.*;
import java.util.stream.*;
public class StreamsAreAutoCloseable {
public static void
main(String[] args) throws IOException{
try(
Stream<String> in = Files.lines(
Paths.get("StreamsAreAutoCloseable.java"));
PrintWriter outfile = new PrintWriter(
"Results.txt"); // [1]
) {
in.skip(5)
.limit(1)
.map(String::toLowerCase)
.forEachOrdered(outfile::println);
} // [2]
}
}
- [1] 你在这里可以看到其他的特性:资源规范头中可以包含多个定义,并且通过分号进行分割(最后一个分号是可选的)。规范头中定义的每个对象都会在 try 语句块运行结束之后调用 close() 方法。
- [2] try-with-resources 里面的 try 语句块可以不包含 catch 或者 finally 语句而独立存在。在这里,IOException 被 main() 方法抛出,所以这里并不需要在 try 后面跟着一个 catch 语句块。
Java 5 中的 Closeable 已经被修改,修改之后的接口继承了 AutoCloseable 接口。所以所有实现了 Closeable 接口的对象,都支持了 try-with-resources 特性。
抛出异常的时候,异常处理系统会按照代码的书写顺序找出“最近”的处理程序。找到匹配的处理程序之后,它就认为异常将得到处理,然后就不再继续查找。
查找的时候并不要求抛出的异常同处理程序所声明的异常完全匹配。派生类的对象也可以匹配其基类的处理程序,就像这样:
应该在下列情况下使用异常:
- 尽可能使用 try-with-resource。
- 在恰当的级别处理问题。(在知道该如何处理的情况下才捕获异常。)
- 解决问题并且重新调用产生异常的方法。
- 进行少许修补,然后绕过异常发生的地方继续执行。
- 用别的数据进行计算,以代替方法预计会返回的值。
- 把当前运行环境下能做的事情尽量做完,然后把相同的异常重抛到更高层。
- 把当前运行环境下能做的事情尽量做完,然后把不同的异常抛到更高层。
- 终止程序。
- 进行简化。(如果你的异常模式使问题变得太复杂,那用起来会非常痛苦也很烦人。)
- 让类库和程序更安全。(这既是在为调试做短期投资,也是在为程序的健壮性做长期投资。)
第十六章 代码校验
业内普遍认为标准 Java 发行版本中的日志包 (java.util.logging) 的设计相当糟糕。 大多数人会选择其他的替代日志包。如 Simple Logging Facade for Java(SLF4J) ,它为多个日志框架提供了一个封装好的调用方式,这些日志框架包括 java.util.logging , logback 和 **log4j **。 SLF4J 允许用户在部署时插入所需的日志框架。
SLF4J 提供了一个复杂的工具来报告程序的信息,它的效率与前面示例中的技术几乎相同。 对于非常简单的信息日志记录,你可以执行以下操作:
// validating/SLF4JLogging.java
import org.slf4j.*;
public class SLF4JLogging {
private static Logger log =
LoggerFactory.getLogger(SLF4JLogging.class);
public static void main(String[] args) {
log.info("hello logging");
}
}
/* Output:
2017-05-09T06:07:53.418
[main] INFO SLF4JLogging - hello logging
*/
第十七章 文件
好像 Java 设计者终于意识到了 Java 使用者多年来的痛苦,在 Java7 中对此引入了巨大的改进。这些新元素被放在 java.nio.file 包下面,过去人们通常把 nio 中的 n 理解为 new 即新的 io,现在更应该当成是 non-blocking 非阻塞 io(io就是input/output输入/输出)。java.nio.file 库终于将 Java 文件操作带到与其他编程语言相同的水平。最重要的是 Java8 新增的 streams 与文件结合使得文件操作编程变得更加优雅。我们将看一下文件操作的两个基本组件:
- 文件或者目录的路径;
- 文件本身。
Files 工具类包含大部分我们需要的目录操作和文件操作方法。出于某种原因,它们没有包含删除目录树相关的方法,因此我们将实现并将其添加到 onjava 库中。
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.io.IOException;
public class RmDir {
public static void rmdir(Path dir) throws IOException {
Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
});
}
}
读取文件
以字节为单位读取文件(FileInputStream)
常用于读二进制文件,如图片、声音、影像等文件。
File file = new File("filePath");
try (InputStream in = new FileInputStream(file);) {
// 一次读4个字节
byte[] bytes = new byte[4];
// 读取到的字节数量
int readCount = 0;
// 读入4个字节到字节数组中
while ((readCount = in.read(bytes)) != -1) {
System.out.write(bytes, 0, readCount);
}
} catch (Exception e) {
e.printStackTrace();
}
以字符为单位读取文件(InputStreamReader)
常用于读文本,数字等类型的文件。
File file = new File("filePath");
try (Reader reader = new InputStreamReader(new FileInputStream(file));) {
// 一次读30个字符
char[] chars = new char[30];
// 读取到的字节数量
int readCount = 0;
while ((readCount = reader.read(chars)) != -1) {
for (int i = 0; i < readCount; i++) {
System.out.print(chars[i]);
}
}
} catch (Exception e) {
e.printStackTrace();
}
以行为单位读取文件内容(BufferedReader)
常用于读面向行的格式化文件。
File file = new File("filePath");
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
String line = null;
// 一次读入一行,直到读入null为文件结束
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
一次读取所有行(Files.readAllLines)
使用nio的Files.readAllLines可以一次性读取所有行
List<String> lines = Files.readAllLines(Paths.get("filePath"));
for (String line : lines) {
System.out.println(line);
}
写入文件
以字节为单位写文件(FileOutputStream)
File file = new File("D:/file.txt");
try (OutputStream out = new FileOutputStream(file);) {
String content = "枫桥夜泊\n张继\n月落乌啼霜满天,\n江枫渔火对愁眠。";
out.write(content.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
以字符为单位写文件(OutputStreamWriter)
File file = new File("D:/file.txt");
try (Writer writer = new OutputStreamWriter(new FileOutputStream(file));) {
String content = "枫桥夜泊\n张继\n月落乌啼霜满天,\n江枫渔火对愁眠。";
writer.write(content);
} catch (IOException e) {
e.printStackTrace();
}
以行为单位写文件(PrintWriter)
try (PrintWriter writer = new PrintWriter(new BufferedWriter(new FileWriter("D:/file.txt")));) {
writer.println(" 枫桥夜泊 "); // 写字符串
writer.print(true); // 写入布尔类型
writer.print(666); // 写入整数类型
writer.println(); // 换行
writer.flush(); // 写入刷新文件
} catch (IOException e) {
e.printStackTrace();
}
另一种以行为单位写文件(FileWriter)
String path = "filePath";
// 第二个参数true表示以追加形式写文件
try (FileWriter writer = new FileWriter(path, true);) {
writer.write("new line");
} catch (IOException e) {
e.printStackTrace();
}
使用RandomAccessFile追加写入
try {
String path = "filePath";
// 打开一个随机访问文件流,按读写方式
RandomAccessFile randomFile = new RandomAccessFile(path, "rw");
// 将写文件指针移到文件尾。
randomFile.seek(randomFile.length());
randomFile.writeBytes("new string");
randomFile.close();
List<String> lines = Files.readAllLines(Paths.get(path));
for (String line : lines) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
FileWriter 与 PrintWriter 的区别
JAVA DOC 的定义
FileWriter is a convenience class for writing character files. The constructors of this class assume that the default character encoding and the default byte-buffer size are acceptable. To specify these values yourself, construct an OutputStreamWriter on a FileOutputStream.
FileWriter 可以很方便的编写字符型文件,它的构造方法设置了默认的字符编码和字节缓冲区大小。如果要自行设置,可以在 FileOutputStream 上构造一个 OutputStreamWriter。 PrintWriter prints formatted representations of objects to a text-output stream. This class implements all of the print methods found in PrintStream. It does not contain methods for writing raw bytes, for which a program should use unencoded byte streams.
PrintWriter 将对象以格式化的形式打印到文本输出流,该类实现了 PrintStream 中的所有 print 方法。它不包含写入原始字节的方法,对于字节,程序应该使用未编码的字节流进行写入。 主要的区别是PrintWriter提供了一些额外的方法用于格式化输出,如println、printf。 如果发生任何I/O异常,FileWriter会抛出IOException,而PrintWriter不会抛出IOException,它会设置一个boolean标志,该标志可以通过调用checkError()方法获取。 PrintWriter在每次写入数据后会自动调用flush()方法,而FileWriter则需要自行调用flush()方法。
第十八章 字符串
字符串操作毫无疑问是计算机程序设计中最常见的行为之一。
在 Java 大展拳脚的 Web 系统中更是如此。在本章中,我们将深入学习在 Java 语言中应用最广泛的 String
类,并研究与之相关的类及工具。
字符串的不可变
String
对象是不可变的。查看 JDK 文档你就会发现,String
类中每一个看起来会修改 String
值的方法,实际上都是创建了一个全新的 String
对象,以包含修改后的字符串内容。而最初的 String
对象则丝毫未动。
看看下面的代码:
// strings/Immutable.java
public class Immutable {
public static String upcase(String s) {
return s.toUpperCase();
}
public static void main(String[] args) {
String q = "howdy";
System.out.println(q); // howdy
String qq = upcase(q);
System.out.println(qq); // HOWDY
System.out.println(q); // howdy
}
}
/* Output:
howdy
HOWDY
howdy
*/ 复制ErrorOK!
当把 q
传递给 upcase()
方法时,实际传递的是引用的一个拷贝。其实,每当把 String 对象作为方法的参数时,都会复制一份引用,而该引用所指向的对象其实一直待在单一的物理位置上,从未动过。
回到 upcase()
的定义,传入其中的引用有了名字 s
,只有 upcase()
运行的时候,局部引用 s
才存在。一旦 upcase()
运行结束,s
就消失了。当然了,upcase()
的返回值,其实是最终结果的引用。这足以说明,upcase()
返回的引用已经指向了一个新的对象,而 q
仍然在原来的位置。
String
的这种行为正是我们想要的。例如:
String s = "asdf";
String x = Immutable.upcase(s);复制ErrorOK!
难道你真的希望 upcase()
方法改变其参数吗?对于一个方法而言,参数是为该方法提供信息的,而不是想让该方法改变自己的。在阅读这段代码时,读者自然会有这样的感觉。这一点很重要,正是有了这种保障,才使得代码易于编写和阅读。
+的重载与 StringBuilder
String
对象是不可变的,你可以给一个 String
对象添加任意多的别名。因为 String
是只读的,所以指向它的任何引用都不可能修改它的值,因此,也就不会影响到其他引用。
不可变性会带来一定的效率问题。为 String
对象重载的 +
操作符就是一个例子。重载的意思是,一个操作符在用于特定的类时,被赋予了特殊的意义(用于 String
的 +
与 +=
是 Java 中仅有的两个重载过的操作符,Java 不允许程序员重载任何其他的操作符 [^1])。
操作符 +
可以用来连接 String
:
// strings/Concatenation.java
public class Concatenation {
public static void main(String[] args) {
String mango = "mango";
String s = "abc" + mango + "def" + 47;
System.out.println(s);
}
}
/* Output:
abcmangodef47
*/复制ErrorOK!
可以想象一下,这段代码是这样工作的:String
可能有一个 append()
方法,它会生成一个新的 String
对象,以包含“abc”与 mango
连接后的字符串。该对象会再创建另一个新的 String
对象,然后与“def”相连,生成另一个新的对象,依此类推。
这种方式当然是可行的,但是为了生成最终的 String
对象,会产生一大堆需要垃圾回收的中间对象。我猜想,Java 设计者一开始就是这么做的(这也是软件设计中的一个教训:除非你用代码将系统实现,并让它运行起来,否则你无法真正了解它会有什么问题),然后他们发现其性能相当糟糕。
想看看以上代码到底是如何工作的吗?可以用 JDK 自带的 javap
工具来反编译以上代码。命令如下:
javap -c Concatenation复制ErrorOK!
这里的 -c
标志表示将生成 JVM 字节码。我们剔除不感兴趣的部分,然后做细微的修改,于是有了以下的字节码:
public static void main(java.lang.String[]);
Code:
Stack=2, Locals=3, Args_size=1
0: ldc #2; //String mango
2: astore_1
3: new #3; //class StringBuilder
6: dup
7: invokespecial #4; //StringBuilder."<init>":()
10: ldc #5; //String abc
12: invokevirtual #6; //StringBuilder.append:(String)
15: aload_1
16: invokevirtual #6; //StringBuilder.append:(String)
19: ldc #7; //String def
21: invokevirtual #6; //StringBuilder.append:(String)
24: bipush 47
26: invokevirtual #8; //StringBuilder.append:(I)
29: invokevirtual #9; //StringBuilder.toString:()
32: astore_2
33: getstatic #10; //Field System.out:PrintStream;
36: aload_2
37: invokevirtual #11; //PrintStream.println:(String)
40: return复制ErrorOK!
如果你有汇编语言的经验,以上代码应该很眼熟(其中的 dup
和 invokevirtual
语句相当于Java虚拟机上的汇编语句。即使你完全不了解汇编语言也无需担心)。需要重点注意的是:编译器自动引入了 java.lang.StringBuilder
类。虽然源代码中并没有使用 StringBuilder
类,但是编译器却自作主张地使用了它,就因为它更高效。
在这里,编译器创建了一个 StringBuilder
对象,用于构建最终的 String
,并对每个字符串调用了一次 append()
方法,共计 4 次。最后调用 toString()
生成结果,并存为 s
(使用的命令为 astore_2
)。
现在,也许你会觉得可以随意使用 String
对象,反正编译器会自动为你做性能优化。可是在这之前,让我们更深入地看看编译器能为我们优化到什么程度。下面的例子采用两种方式生成一个 String
:方法一使用了多个 String
对象;方法二在代码中使用了 StringBuilder
。
// strings/WhitherStringBuilder.java
public class WhitherStringBuilder {
public String implicit(String[] fields) {
String result = "";
for(String field : fields) {
result += field;
}
return result;
}
public String explicit(String[] fields) {
StringBuilder result = new StringBuilder();
for(String field : fields) {
result.append(field);
}
return result.toString();
}
}复制ErrorOK!
现在运行 javap -c WitherStringBuilder
,可以看到两种不同方法(我已经去掉不相关的细节)对应的字节码。首先是 implicit()
方法:
public java.lang.String implicit(java.lang.String[]);
0: ldc #2 // String
2: astore_2
3: aload_1
4: astore_3
5: aload_3
6: arraylength
7: istore 4
9: iconst_0
10: istore 5
12: iload 5
14: iload 4
16: if_icmpge 51
19: aload_3
20: iload 5
22: aaload
23: astore 6
25: new #3 // StringBuilder
28: dup
29: invokespecial #4 // StringBuilder."<init>"
32: aload_2
33: invokevirtual #5 // StringBuilder.append:(String)
36: aload 6
38: invokevirtual #5 // StringBuilder.append:(String;)
41: invokevirtual #6 // StringBuilder.toString:()
44: astore_2
45: iinc 5, 1
48: goto 12
51: aload_2
52: areturn复制ErrorOK!
注意从第 16 行到第 48 行构成了一个循环体。第 16 行:对堆栈中的操作数进行“大于或等于的整数比较运算”,循环结束时跳转到第 51 行。第 48 行:重新回到循环体的起始位置(第 12 行)。注意:StringBuilder
是在循环内构造的,这意味着每进行一次循环,会创建一个新的 StringBuilder
对象。
下面是 explicit()
方法对应的字节码:
public java.lang.String explicit(java.lang.String[]);
0: new #3 // StringBuilder
3: dup
4: invokespecial #4 // StringBuilder."<init>"
7: astore_2
8: aload_1
9: astore_3
10: aload_3
11: arraylength
12: istore 4
14: iconst_0
15: istore 5
17: iload 5
19: iload 4
21: if_icmpge 43
24: aload_3
25: iload 5
27: aaload
28: astore 6
30: aload_2
31: aload 6
33: invokevirtual #5 // StringBuilder.append:(String)
36: pop
37: iinc 5, 1
40: goto 17
43: aload_2
44: invokevirtual #6 // StringBuilder.toString:()
47: areturn复制ErrorOK!
可以看到,不仅循环部分的代码更简短、更简单,而且它只生成了一个 StringBuilder
对象。显式地创建 StringBuilder
还允许你预先为其指定大小。如果你已经知道最终字符串的大概长度,那预先指定 StringBuilder
的大小可以避免频繁地重新分配缓冲。
因此,当你为一个类编写 toString()
方法时,如果字符串操作比较简单,那就可以信赖编译器,它会为你合理地构造最终的字符串结果。但是,如果你要在 toString()
方法中使用循环,且可能有性能问题,那么最好自己创建一个 StringBuilder
对象,用它来构建最终结果。请参考以下示例:
// strings/UsingStringBuilder.java
import java.util.*;
import java.util.stream.*;
public class UsingStringBuilder {
public static String string1() {
Random rand = new Random(47);
StringBuilder result = new StringBuilder("[");
for(int i = 0; i < 25; i++) {
result.append(rand.nextInt(100));
result.append(", ");
}
result.delete(result.length()-2, result.length());
result.append("]");
return result.toString();
}
public static String string2() {
String result = new Random(47)
.ints(25, 0, 100)
.mapToObj(Integer::toString)
.collect(Collectors.joining(", "));
return "[" + result + "]";
}
public static void main(String[] args) {
System.out.println(string1());
System.out.println(string2());
}
}
/* Output:
[58, 55, 93, 61, 61, 29, 68, 0, 22, 7, 88, 28, 51, 89,
9, 78, 98, 61, 20, 58, 16, 40, 11, 22, 4]
[58, 55, 93, 61, 61, 29, 68, 0, 22, 7, 88, 28, 51, 89,
9, 78, 98, 61, 20, 58, 16, 40, 11, 22, 4]
*/ 复制ErrorOK!
在方法 string1()
中,最终结果是用 append()
语句拼接起来的。如果你想走捷径,例如:append(a + ": " + c)
,编译器就会掉入陷阱,从而为你另外创建一个 StringBuilder
对象处理括号内的字符串操作。如果拿不准该用哪种方式,随时可以用 javap
来分析你的程序。
StringBuilder
提供了丰富而全面的方法,包括 insert()
、replace()
、substring()
,甚至还有reverse()
,但是最常用的还是 append()
和 toString()
。还有 delete()
,上面的例子中我们用它删除最后一个逗号和空格,以便添加右括号。
string2()
使用了 Stream
,这样代码更加简洁美观。可以证明,Collectors.joining()
内部也是使用的 StringBuilder
,这种写法不会影响性能!
StringBuilder
是 Java SE5 引入的,在这之前用的是 StringBuffer
。后者是线程安全的(参见并发编程),因此开销也会大些。使用 StringBuilder
进行字符串操作更快一点。
意外递归
Java 中的每个类从根本上都是继承自 Object
,标准集合类也是如此,它们都有 toString()
方法,并且覆盖了该方法,使得它生成的 String
结果能够表达集合自身,以及集合包含的对象。例如 ArrayList.toString()
,它会遍历 ArrayList
中包含的所有对象,调用每个元素上的 toString()
方法:
// strings/ArrayListDisplay.java
import java.util.*;
import java.util.stream.*;
import generics.coffee.*;
public class ArrayListDisplay {
public static void main(String[] args) {
List<Coffee> coffees =
Stream.generate(new CoffeeSupplier())
.limit(10)
.collect(Collectors.toList());
System.out.println(coffees);
}
}
/* Output:
[Americano 0, Latte 1, Americano 2, Mocha 3, Mocha 4,
Breve 5, Americano 6, Latte 7, Cappuccino 8, Cappuccino 9]
*/ 复制ErrorOK!
如果你希望 toString()
打印出类的内存地址,也许你会考虑使用 this
关键字:
// strings/InfiniteRecursion.java
// Accidental recursion
// {ThrowsException}
// {VisuallyInspectOutput} Throws very long exception
import java.util.*;
import java.util.stream.*;
public class InfiniteRecursion {
@Override
public String toString() {
return " InfiniteRecursion address: " + this + "\n"
}
public static void main(String[] args) {
Stream.generate(InfiniteRecursion::new)
.limit(10)
.forEach(System.out::println);
}
} 复制ErrorOK!
当你创建了 InfiniteRecursion
对象,并将其打印出来的时候,你会得到一串很长的异常信息。如果你将该 InfiniteRecursion
对象存入一个 ArrayList
中,然后打印该 ArrayList
,同样也会抛出异常。其实,当运行到如下代码时:
"InfiniteRecursion address: " + this 复制ErrorOK!
这里发生了自动类型转换,由 InfiniteRecursion
类型转换为 String
类型。因为编译器发现一个 String
对象后面跟着一个 “+”,而 “+” 后面的对象不是 String
,于是编译器试着将 this
转换成一个 String
。它怎么转换呢?正是通过调用 this
上的 toString()
方法,于是就发生了递归调用。
如果你真的想要打印对象的内存地址,应该调用 Object.toString()
方法,这才是负责此任务的方法。所以,不要使用 this
,而是应该调用 super.toString()
方法。
字符串操作
以下是 String
对象具备的一些基本方法。重载的方法归纳在同一行中:
方法 | 参数,重载版本 | 作用 |
---|---|---|
构造方法 | 默认版本,String ,StringBuilder ,StringBuffer ,char 数组,byte 数组 |
创建String 对象 |
length() |
String 中字符的个数 |
|
charAt() |
int 索引 |
获取String 中索引位置上的char |
getChars() ,getBytes() |
待复制部分的开始和结束索引,复制的目标数组,目标数组的开始索引 | 复制char 或byte 到一个目标数组中 |
toCharArray() |
生成一个char[] ,包含String 中的所有字符 |
|
equals() ,equalsIgnoreCase() |
与之进行比较的String |
比较两个String 的内容是否相同。如果相同,结果为true |
compareTo() ,compareToIgnoreCase() |
与之进行比较的String |
按词典顺序比较String 的内容,比较结果为负数、零或正数。注意,大小写不等价 |
contains() |
要搜索的CharSequence |
如果该String 对象包含参数的内容,则返回true |
contentEquals() |
与之进行比较的CharSequence 或StringBuffer |
如果该String 对象与参数的内容完全一致,则返回true |
isEmpty() |
返回boolean 结果,以表明String 对象的长度是否为0 |
|
regionMatches() |
该String 的索引偏移量,另一个String 及其索引偏移量,要比较的长度。重载版本增加了“忽略大小写”功能 |
返回boolean 结果,以表明所比较区域是否相等 |
startsWith() |
可能的起始String 。重载版本在参数中增加了偏移量 |
返回boolean 结果,以表明该String 是否以传入参数开始 |
endsWith() |
该String 可能的后缀String |
返回boolean 结果,以表明此参数是否是该字符串的后缀 |
indexOf() ,lastIndexOf() |
重载版本包括:char ,char 与起始索引,String ,String 与起始索引 |
如果该String 并不包含此参数,就返回-1;否则返回此参数在String 中的起始索引。lastIndexOf ()是从后往前搜索 |
matches() |
一个正则表达式 | 返回boolean 结果,以表明该String 和给出的正则表达式是否匹配 |
split() |
一个正则表达式。可选参数为需要拆分的最大数量 | 按照正则表达式拆分String ,返回一个结果数组 |
join() (Java8引入的) |
分隔符,待拼字符序列。用分隔符将字符序列拼接成一个新的String |
用分隔符拼接字符片段,产生一个新的String |
substring() (即subSequence() ) |
重载版本:起始索引;起始索引+终止索引 | 返回一个新的String 对象,以包含参数指定的子串 |
concat() |
要连接的String |
返回一个新的String 对象,内容为原始String 连接上参数String |
replace() |
要替换的字符,用来进行替换的新字符。也可以用一个CharSequence 替换另一个CharSequence |
返回替换字符后的新String 对象。如果没有替换发生,则返回原始的String 对象 |
replaceFirst() |
要替换的正则表达式,用来进行替换的String |
返回替换首个目标字符串后的String 对象 |
replaceAll() |
要替换的正则表达式,用来进行替换的String |
返回替换所有目标字符串后的String 对象 |
toLowerCase() ,toUpperCase() |
将字符的大小写改变后,返回一个新的String 对象。如果没有任何改变,则返回原始的String 对象 |
|
trim() |
将String 两端的空白符删除后,返回一个新的String 对象。如果没有任何改变,则返回原始的String 对象 |
|
valueOf() (static ) |
重载版本:Object ;char[] ;char[] ,偏移量,与字符个数;boolean ;char ;int ;long ;float ;double |
返回一个表示参数内容的String |
intern() |
为每个唯一的字符序列生成一个且仅生成一个String 引用 |
|
format() |
要格式化的字符串,要替换到格式化字符串的参数 | 返回格式化结果String |
从这个表可以看出,当需要改变字符串的内容时,String
类的方法都会返回一个新的 String
对象。同时,如果内容不改变,String
方法只是返回原始对象的一个引用而已。这可以节约存储空间以及避免额外的开销。
本章稍后还将介绍正则表达式在 String
方法中的应用。
格式化输出
在长久的等待之后,Java SE5 终于推出了 C 语言中 printf()
风格的格式化输出这一功能。这不仅使得控制输出的代码更加简单,同时也给与Java开发者对于输出格式与排列更强大的控制能力。
printf()
C 语言的 printf()
并不像 Java 那样连接字符串,它使用一个简单的格式化字符串,加上要插入其中的值,然后将其格式化输出。 printf()
并不使用重载的 +
操作符(C语言没有重载)来连接引号内的字符串或字符串变量,而是使用特殊的占位符来表示数据将来的位置。而且它还将插入格式化字符串的参数,以逗号分隔,排成一行。例如:
System.out.printf("Row 1: [%d %f]%n", x, y);复制ErrorOK!
这一行代码在运行的时候,首先将 x
的值插入到 %d_
的位置,然后将 y
的值插入到 %f
的位置。这些占位符叫做*格式修饰符*,它们不仅指明了插入数据的位置,同时还指明了将会插入什么类型的变量,以及如何格式化。在这个例子中 %d
表示 x
是一个整数,%f
表示 y
是一个浮点数(float
或者 double
)。
System.out.format()
Java SE5 引入了 format()
方法,可用于 PrintStream
或者 PrintWriter
对象(你可以在 附录:流式 I/O 了解更多内容),其中也包括 System.out
对象。format()
方法模仿了 C 语言的 printf()
。如果你比较怀旧的话,也可以使用 printf()
。以下是一个简单的示例:
// strings/SimpleFormat.java
public class SimpleFormat {
public static void main(String[] args) {
int x = 5;
double y = 5.332542;
// The old way:
System.out.println("Row 1: [" + x + " " + y + "]");
// The new way:
System.out.format("Row 1: [%d %f]%n", x, y);
// or
System.out.printf("Row 1: [%d %f]%n", x, y);
}
}
/* Output:
Row 1: [5 5.332542]
Row 1: [5 5.332542]
Row 1: [5 5.332542]
*/复制ErrorOK!
可以看到,format()
和 printf()
是等价的,它们只需要一个简单的格式化字符串,加上一串参数即可,每个参数对应一个格式修饰符。
String
类也有一个 static format()
方法,可以格式化字符串。
Formatter
类
在 Java 中,所有的格式化功能都是由 java.util.Formatter
类处理的。可以将 Formatter
看做一个翻译器,它将你的格式化字符串与数据翻译成需要的结果。当你创建一个 Formatter
对象时,需要向其构造器传递一些信息,告诉它最终的结果将向哪里输出:
// strings/Turtle.java
import java.io.*;
import java.util.*;
public class Turtle {
private String name;
private Formatter f;
public Turtle(String name, Formatter f) {
this.name = name;
this.f = f;
}
public void move(int x, int y) {
f.format("%s The Turtle is at (%d,%d)%n",
name, x, y);
}
public static void main(String[] args) {
PrintStream outAlias = System.out;
Turtle tommy = new Turtle("Tommy",
new Formatter(System.out));
Turtle terry = new Turtle("Terry",
new Formatter(outAlias));
tommy.move(0,0);
terry.move(4,8);
tommy.move(3,4);
terry.move(2,5);
tommy.move(3,3);
terry.move(3,3);
}
}
/* Output:
Tommy The Turtle is at (0,0)
Terry The Turtle is at (4,8)
Tommy The Turtle is at (3,4)
Terry The Turtle is at (2,5)
Tommy The Turtle is at (3,3)
Terry The Turtle is at (3,3)
*/复制ErrorOK!
格式化修饰符 %s
表明这里需要 String
参数。
所有的 tommy
都将输出到 System.out
,而所有的 terry
则都输出到 System.out
的一个别名中。Formatter
的重载构造器支持输出到多个路径,不过最常用的还是 PrintStream()
(如上例)、OutputStream
和 File
。你可以在 附录:流式 I/O 中了解更多信息。
格式化修饰符
在插入数据时,如果想要优化空格与对齐,你需要更精细复杂的格式修饰符。以下是其通用语法:
%[argument_index$][flags][width][.precision]conversion 复制ErrorOK!
最常见的应用是控制一个字段的最小长度,这可以通过指定 width 来实现。Formatter
对象通过在必要时添加空格,来确保一个字段至少达到设定长度。默认情况下,数据是右对齐的,不过可以通过使用 -
标志来改变对齐方向。
与 width 相对的是 precision*,用于指定最大长度。*width 可以应用于各种类型的数据转换,并且其行为方式都一样。precision 则不然,当应用于不同类型的数据转换时,precision 的意义也不同。在将 precision 应用于 String
时,它表示打印 string
时输出字符的最大数量。而在将 precision 应用于浮点数时,它表示小数部分要显示出来的位数(默认是 6 位小数),如果小数位数过多则舍入,太少则在尾部补零。由于整数没有小数部分,所以 precision 无法应用于整数,如果你对整数应用 *precision*,则会触发异常。
下面的程序应用格式修饰符来打印一个购物收据。这是 Builder 设计模式的一个简单实现,即先创建一个初始对象,然后逐渐添加新东西,最后调用 build()
方法完成构建:
// strings/ReceiptBuilder.java
import java.util.*;
public class ReceiptBuilder {
private double total = 0;
private Formatter f =
new Formatter(new StringBuilder());
public ReceiptBuilder() {
f.format(
"%-15s %5s %10s%n", "Item", "Qty", "Price");
f.format(
"%-15s %5s %10s%n", "----", "---", "-----");
}
public void add(String name, int qty, double price) {
f.format("%-15.15s %5d %10.2f%n", name, qty, price);
total += price * qty;
}
public String build() {
f.format("%-15s %5s %10.2f%n", "Tax", "",
total * 0.06);
f.format("%-15s %5s %10s%n", "", "", "-----");
f.format("%-15s %5s %10.2f%n", "Total", "",
total * 1.06);
return f.toString();
}
public static void main(String[] args) {
ReceiptBuilder receiptBuilder =
new ReceiptBuilder();
receiptBuilder.add("Jack's Magic Beans", 4, 4.25);
receiptBuilder.add("Princess Peas", 3, 5.1);
receiptBuilder.add(
"Three Bears Porridge", 1, 14.29);
System.out.println(receiptBuilder.build());
}
}
/* Output:
Item Qty Price
---- --- -----
Jack's Magic Be 4 4.25
Princess Peas 3 5.10
Three Bears Por 1 14.29
Tax 2.80
-----
Total 49.39
*/ 复制ErrorOK!
通过传入一个 StringBuilder
对象到 Formatter
的构造器,我们指定了一个容器来构建目标 String
。你也可以通过不同的构造器参数,把结果输出到标准输出,甚至是一个文件里。
正如你所见,通过相当简洁的语法,Formatter
提供了对空格与对齐的强大控制能力。在该程序中,为了恰当地控制间隔,格式化字符串被重复利用了多遍。
Formatter
转换
下面的表格展示了最常用的类型转换:
类型 | 含义 |
---|---|
d |
整型(十进制) |
c |
Unicode字符 |
b |
Boolean值 |
s |
String |
f |
浮点数(十进制) |
e |
浮点数(科学计数) |
x |
整型(十六进制) |
h |
散列码(十六进制) |
% |
字面值“%” |
下面的程序演示了这些转换是如何工作的:
// strings/Conversion.java
import java.math.*;
import java.util.*;
public class Conversion {
public static void main(String[] args) {
Formatter f = new Formatter(System.out);
char u = 'a';
System.out.println("u = 'a'");
f.format("s: %s%n", u);
// f.format("d: %d%n", u);
f.format("c: %c%n", u);
f.format("b: %b%n", u);
// f.format("f: %f%n", u);
// f.format("e: %e%n", u);
// f.format("x: %x%n", u);
f.format("h: %h%n", u);
int v = 121;
System.out.println("v = 121");
f.format("d: %d%n", v);
f.format("c: %c%n", v);
f.format("b: %b%n", v);
f.format("s: %s%n", v);
// f.format("f: %f%n", v);
// f.format("e: %e%n", v);
f.format("x: %x%n", v);
f.format("h: %h%n", v);
BigInteger w = new BigInteger("50000000000000");
System.out.println(
"w = new BigInteger(\"50000000000000\")");
f.format("d: %d%n", w);
// f.format("c: %c%n", w);
f.format("b: %b%n", w);
f.format("s: %s%n", w);
// f.format("f: %f%n", w);
// f.format("e: %e%n", w);
f.format("x: %x%n", w);
f.format("h: %h%n", w);
double x = 179.543;
System.out.println("x = 179.543");
// f.format("d: %d%n", x);
// f.format("c: %c%n", x);
f.format("b: %b%n", x);
f.format("s: %s%n", x);
f.format("f: %f%n", x);
f.format("e: %e%n", x);
// f.format("x: %x%n", x);
f.format("h: %h%n", x);
Conversion y = new Conversion();
System.out.println("y = new Conversion()");
// f.format("d: %d%n", y);
// f.format("c: %c%n", y);
f.format("b: %b%n", y);
f.format("s: %s%n", y);
// f.format("f: %f%n", y);
// f.format("e: %e%n", y);
// f.format("x: %x%n", y);
f.format("h: %h%n", y);
boolean z = false;
System.out.println("z = false");
// f.format("d: %d%n", z);
// f.format("c: %c%n", z);
f.format("b: %b%n", z);
f.format("s: %s%n", z);
// f.format("f: %f%n", z);
// f.format("e: %e%n", z);
// f.format("x: %x%n", z);
f.format("h: %h%n", z);
}
}
/* Output:
u = 'a'
s: a
c: a
b: true
h: 61
v = 121
d: 121
c: y
b: true
s: 121
x: 79
h: 79
w = new BigInteger("50000000000000")
d: 50000000000000
b: true
s: 50000000000000
x: 2d79883d2000
h: 8842a1a7
x = 179.543
b: true
s: 179.543
f: 179.543000
e: 1.795430e+02
h: 1ef462c
y = new Conversion()
b: true
s: Conversion@15db9742
h: 15db9742
z = false
b: false
s: false
h: 4d5
*/复制ErrorOK!
被注释的代码表示,针对相应类型的变量,这些转换是无效的。如果执行这些转换,则会触发异常。
注意,程序中的每个变量都用到了 b
转换。虽然它对各种类型都是合法的,但其行为却不一定与你想象的一致。对于 boolean
基本类型或 Boolean
对象,其转换结果是对应的 true
或 false
。但是,对其他类型的参数,只要该参数不为 null
,其转换结果永远都是 true
。即使是数字 0,转换结果依然为 true
,而这在其他语言中(包括C),往往转换为 false
。所以,将 b
应用于非布尔类型的对象时请格外小心。
还有许多不常用的类型转换与格式修饰符选项,你可以在 JDK 文档中的 Formatter
类部分找到它们。
String.format()
Java SE5 也参考了 C 中的 sprintf()
方法,以生成格式化的 String
对象。String.format()
是一个 static
方法,它接受与 Formatter.format()
方法一样的参数,但返回一个 String
对象。当你只需使用一次 format()
方法的时候,String.format()
用起来很方便
第十九章 类型信息
RTTI(RunTime Type Information,运行时类型信息)能够在程序运行时发现和使用类型信息
RTTI 把我们从只能在编译期进行面向类型操作的禁锢中解脱了出来,并且让我们可以使用某些非常强大的程序。对 RTTI 的需要,揭示了面向对象设计中许多有趣(并且复杂)的特性,同时也带来了关于如何组织程序的基本问题。
本章将讨论 Java 是如何在运行时识别对象和类信息的。主要有两种方式:
- “传统的” RTTI:假定我们在编译时已经知道了所有的类型;
- “反射”机制:允许我们在运行时发现和使用类的信息。
严格来说,Stream
实际上是把放入其中的所有对象都当做 Object
对象来持有,只是取元素时会自动将其类型转为 Shape
。这也是 RTTI 最基本的使用形式,因为在 Java 中,所有类型转换的正确性检查都是在运行时进行的。这也正是 RTTI 的含义所在:在运行时,识别一个对象的类型。
类是程序的一部分,每个类都有一个 Class
对象。换言之,每当我们编写并且编译了一个新类,就会产生一个 Class
对象(更恰当的说,是被保存在一个同名的 .class
文件中)。为了生成这个类的对象,Java 虚拟机 (JVM) 先会调用”类加载器”子系统把这个类加载到内存中。
类加载器首先会检查这个类的 Class
对象是否已经加载,如果尚未加载,默认的类加载器就会根据类名查找 .class
文件(如果有附加的类加载器,这时候可能就会在数据库中或者通过其它方式获得字节码)。这个类的字节码被加载后,JVM 会对其进行验证,确保它没有损坏,并且不包含不良的 Java 代码(这是 Java 安全防范的一种措施)。一旦某个类的 Class
对象被载入内存,它就可以用来创建这个类的所有对象。
其实构造器也是类的静态方法,虽然构造器前面并没有 static
关键字。所以,使用 new
操作符创建类的新对象,这个操作也算作对类的静态成员引用。
无论何时,只要你想在运行时使用类型信息,就必须先得到那个 Class
对象的引用。Class.forName()
就是实现这个功能的一个便捷途径,因为使用该方法你不需要先持有这个类型 的对象。但是,如果你已经拥有了目标类的对象,那就可以通过调用 getClass()
方法来获取 Class
引用了,这个方法来自根类 Object
,它将返回表示该对象实际类型的 Class
对象的引用。
getName()
来产生完整类名,
getSimpleName()
产生不带包名的类名,
getCanonicalName()
也是产生完整类名(除内部类和数组外,对大部分类产生的结果与getName()
相同)。
isInterface()
用于判断某个Class
对象代表的是否为一个接口。
Class.getInterface()
方法返回的是存放Class
对象的数组,里面的Class
对象表示的是那个类实现的接口
getSuperclass()
方法来得到父类的Class
对象,再用父类的Class
对象调用该方法,重复多次,你就可以得到一个对象完整的类继承结构。
newInstance()
方法是实现“虚拟构造器”的一种途径,虚拟构造器可以让你在不知道一个类的确切类型的时候,创建这个类的对象。
注意,有一点很有趣:当使用 .class
来创建对 Class
对象的引用时,不会自动地初始化该 Class
对象。为了使用类而做的准备工作实际包含三个步骤:
- 加载,这是由类加载器执行的。该步骤将查找字节码(通常在 classpath 所指定的路径中查找,但这并非是必须的),并从这些字节码中创建一个
Class
对象。 - 链接。在链接阶段将验证类中的字节码,为
static
字段分配存储空间,并且如果需要的话,将解析这个类创建的对其他类的所有引用。 - 初始化。如果该类具有超类,则先初始化超类,执行
static
初始化器和static
初始化块。
直到第一次引用一个 static
方法(构造器隐式地是 static
)或者非常量的 static
字段,才会进行类初始化。
初始化有效地实现了尽可能的“惰性”,从对 initable
引用的创建中可以看到,仅使用 .class
语法来获得对类对象的引用不会引发初始化。但与此相反,使用 Class.forName()
来产生 Class
引用会立即就进行初始化,如 initable3
。
了在使用 Class
引用时放松限制,我们使用了通配符,它是 Java 泛型中的一部分。通配符就是 ?
,表示“任何事物”。因此,我们可以在上例的普通 Class
引用中添加通配符,并产生相同的结果:
// typeinfo/WildcardClassReferences.java
public class WildcardClassReferences {
public static void main(String[] args) {
Class<?> intClass = int.class;
intClass = double.class;
}
}复制ErrorOK!
使用 Class
比单纯使用 Class
要好,虽然它们是等价的,并且单纯使用 Class
不会产生编译器警告信息。使用 Class
的好处是它表示你并非是碰巧或者由于疏忽才使用了一个非具体的类引用,而是特意为之。
第二十章 泛型
这个概念称为元组*,它是将一组对象直接打包存储于单一对象中。可以从该对象读取其中的元素,但不允许向其中存储新对象(这个概念也称为 *数据传输对象 或 信使 )。
下面是另一个实现 Supplier
接口的例子,它负责生成 Fibonacci 数列:
// generics/Fibonacci.java
// Generate a Fibonacci sequence
import java.util.function.*;
import java.util.stream.*;
public class Fibonacci implements Supplier<Integer> {
private int count = 0;
@Override
public Integer get() { return fib(count++); }
private int fib(int n) {
if(n < 2) return 1;
return fib(n-2) + fib(n-1);
}
public static void main(String[] args) {
Stream.generate(new Fibonacci())
.limit(18)
.map(n -> n + " ")
.forEach(System.out::print);
}
}
泛型方法独立于类而改变方法。作为准则,请“尽可能”使用泛型方法。通常将单个方法泛型化要比将整个类泛型化更清晰易懂。
流式编程实现Fibonacci
// streams/Fibonacci.java
import java.util.stream.*;
public class Fibonacci {
int x = 1;
Stream<Integer> numbers() {
return Stream.iterate(0, i -> {
int result = x + i;
x = i;
return result;
});
}
public static void main(String[] args) {
new Fibonacci().numbers()
.skip(20) // 过滤前 20 个
.limit(10) // 然后取 10 个
.forEach(System.out::println);
}
}
如果方法是 static 的,则无法访问该类的泛型类型参数,因此,如果使用了泛型类型参数,则它必须是泛型方法。
要定义泛型方法,请将泛型参数列表放置在返回值之前,如下所示:
// generics/GenericMethods.java
public class GenericMethods {
public <T> void f(T x) {
System.out.println(x.getClass().getName());
}
public static void main(String[] args) {
GenericMethods gm = new GenericMethods();
gm.f("");
gm.f(1);
gm.f(1.0);
gm.f(1.0F);
gm.f('c');
gm.f(gm);
}
}
/* Output:
java.lang.String
java.lang.Integer
java.lang.Double
java.lang.Float
java.lang.Character
GenericMethods
*/
对于泛型类,必须在实例化该类时指定类型参数。使用泛型方法时,通常不需要指定参数类型,因为编译器会找出这些类型。 这称为 *类型参数推断*。因此,对 f()
的调用看起来像普通的方法调用,并且 f()
看起来像被重载了无数次一样。它甚至会接受 GenericMethods 类型的参数。
如果使用基本类型调用 f()
,自动装箱就开始起作用,自动将基本类型包装在它们对应的包装类型中。
变长参数和泛型方法
泛型方法和变长参数列表可以很好地共存:
// generics/GenericVarargs.java
import java.util.ArrayList;
import java.util.List;
public class GenericVarargs {
@SafeVarargs
public static <T> List<T> makeList(T... args) {
List<T> result = new ArrayList<>();
for (T item : args)
result.add(item);
return result;
}
public static void main(String[] args) {
List<String> ls = makeList("A");
System.out.println(ls);
ls = makeList("A", "B", "C");
System.out.println(ls);
ls = makeList(
"ABCDEFFHIJKLMNOPQRSTUVWXYZ".split(""));
System.out.println(ls);
}
}
/* Output:
[A]
[A, B, C]
[A, B, C, D, E, F, F, H, I, J, K, L, M, N, O, P, Q, R,
S, T, U, V, W, X, Y, Z]
*/
此处显示的 makeList()
方法产生的功能与标准库的 java.util.Arrays.asList()
方法相同。
@SafeVarargs
注解保证我们不会对变长参数列表进行任何修改,这是正确的,因为我们只从中读取。如果没有此注解,编译器将无法知道这些并会发出警告。
Java 泛型是使用擦除实现的。这意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。因此,List<String>
和 List<Integer>
在运行时实际上是相同的类型。它们都被擦除成原生类型 List
。
边界 `声明 T 必须是 HasF 类型或其子类。如果情况确实如此,就可以安全地在 **obj** 上调用
f()` 方法。
我们说泛型类型参数会擦除到它的第一个边界(可能有多个边界,稍后你将看到)。我们还提到了类型参数的擦除。编译器实际上会把类型参数替换为它的擦除,就像上面的示例,T 擦除到了 HasF,就像在类的声明中用 HasF 替换了 T 一样。
你想要使用泛型T中的方法和属性就首先需要确定该泛型中有,因此你需要泛型T继承一个含有该属性与方法的类,为它定义边界
例如, List<T>
这样的类型注解会被擦除为 List,普通的类型变量在未指定边界的情况下会被擦除为 Object。
第二十一章 数组
所以一个 ArrayList 的效率不如数组
Arrays.toString() 来将数组转换为可读字符串
这个例子使用 Arrays.deepToString() 方法,将多维数组转换成 String 类型,就像输出中显示的那样。
Arrays.setAll(a[i][j], n -> val++);
Java 8 增加了 Arrays.setAll() 方法,其使用生成器来生成插入数组中的值。此生成器符合函数式接口 IntUnaryOperator ,只使用一个非 默认 的方法 ApplyAsint(int操作数) 。 Arrays.setAll() 传递当前数组索引作为操作数,因此一个选项是提供 n -> n 的 lambda 表达式来显示数组的索引(在上面的代码中很容易尝试)。这里,我们忽略索引,只是插入递增计数器的值。
Arrays工具类
您已经看到了 java.util.Arrays 中的 fill() 和 setAll()/parallelSetAll() 。该类包含许多其他有用的 静态 程序方法,我们将对此进行研究。
概述:
- asList(): 获取任何序列或数组,并将其转换为一个 列表集合 (集合章节介绍了此方法)。
- copyOf():以新的长度创建现有数组的新副本。
- copyOfRange():创建现有数组的一部分的新副本。
- equals():比较两个数组是否相等。
- deepEquals():多维数组的相等性比较。
- stream():生成数组元素的流。
- hashCode():生成数组的哈希值(您将在附录中了解这意味着什么:理解equals()和hashCode())。
- deepHashCode(): 多维数组的哈希值。
- sort():排序数组
- parallelSort():对数组进行并行排序,以提高速度。
- binarySearch():在已排序的数组中查找元素。
- parallelPrefix():使用提供的函数并行累积(以获得速度)。基本上,就是数组的reduce()。
- spliterator():从数组中产生一个Spliterator;这是本书没有涉及到的流的高级部分。
- toString():为数组生成一个字符串表示。你在整个章节中经常看到这种用法。
- deepToString():为多维数组生成一个字符串。你在整个章节中经常看到这种用法。对于所有基本类型和对象,所有这些方法都是重载的。
parallelSort() 算法将大数组拆分成更小的数组,直到数组大小达到极限,然后使用普通的 Arrays .sort() 方法。然后合并结果。该算法需要不大于原始数组的额外工作空间。
一旦数组被排序,您就可以通过使用 Arrays.binarySearch() 来执行对特定项的快速搜索。但是,如果尝试在未排序的数组上使用 binarySearch(),结果是不可预测的。说明使用二分查找的前提是数组已经有序
第二十二章 枚举
基本 enum 特性
我们已经在初始化和清理 这章章看到,调用 enum 的 values() 方法,可以遍历 enum 实例 .values() 方法返回 enum 实例的数组,而且该数组中的元素严格保持其在 enum 中声明时的顺序,因此你可以在循环中使用 values() 返回的数组。
创建 enum 时,编译器会为你生成一个相关的类,这个类继承自 Java.lang.Enum。下面的例子演示了 Enum 提供的一些功能:
// enums/EnumClass.java
// Capabilities of the Enum class
enum Shrubbery { GROUND, CRAWLING, HANGING }
public class EnumClass {
public static void main(String[] args) {
for(Shrubbery s : Shrubbery.values()) {
System.out.println(
s + " ordinal: " + s.ordinal());
System.out.print(
s.compareTo(Shrubbery.CRAWLING) + " ");
System.out.print(
s.equals(Shrubbery.CRAWLING) + " ");
System.out.println(s == Shrubbery.CRAWLING);
System.out.println(s.getDeclaringClass());
System.out.println(s.name());
System.out.println("********************");
}
// Produce an enum value from a String name:
for(String s :
"HANGING CRAWLING GROUND".split(" ")) {
Shrubbery shrub =
Enum.valueOf(Shrubbery.class, s);
System.out.println(shrub);
}
}
}
使用 static import 能够将 enum 实例的标识符带入当前的命名空间,所以无需再用 enum 类型来修饰 enum 实例。这是一个好的想法吗?或者还是显式地修饰 enum 实例更好?这要看代码的复杂程度了。编译器可以确保你使用的是正确的类型,所以唯一需要担心的是,使用静态导入会不会导致你的代码令人难以理解。多数情况下,使用 static import 还是有好处的,不过,程序员还是应该对具体情况进行具体分析。
注意,在定义 enum 的同一个文件中,这种技巧无法使用,如果是在默认包中定义 enum,这种技巧也无法使用(在 Sun 内部对这一点显然也有不同意见)。
public enum SpicinessEnum {
NOT, MILD, MEDIUM, HOT, FLAMING
}
// enums/Burrito2.java
// {java enums.Burrito2}
package enums;
import static enums.SpicinessEnum.*;
public class Burrito2 {
我们已经知道,所有的 enum 都继承自 Java.lang.Enum 类。由于 Java 不支持多重继承,所以你的 enum 不能再继承其他类:
enum NotPossible extends Pet { ... // Won't work
第二十三章 注解
元注解
Java 语言中目前有 5 种标准注解(前面介绍过),以及 5 种元注解。元注解用于注解其他的注解
注解 | 解释 |
---|---|
@Target | 表示注解可以用于哪些地方。可能的 ElementType 参数包括: CONSTRUCTOR:构造器的声明 FIELD:字段声明(包括 enum 实例) LOCAL_VARIABLE:局部变量声明 METHOD:方法声明 PACKAGE:包声明 PARAMETER:参数声明 TYPE:类、接口(包括注解类型)或者 enum 声明 |
@Retention | 表示注解信息保存的时长。可选的 RetentionPolicy 参数包括: SOURCE:注解将被编译器丢弃 CLASS:注解在 class 文件中可用,但是会被 VM 丢弃。 RUNTIME:VM 将在运行期也保留注解,因此可以通过反射机制读取注解的信息。 |
@Documented | 将此注解保存在 Javadoc 中 |
@Inherited | 允许子类继承父类的注解 |
@Repeatable | 允许一个注解可以被使用一次或者多次(Java 8)。 |
大多数时候,程序员定义自己的注解,并编写自己的处理器来处理他们。
注解元素
在 UseCase.java 中定义的 @UseCase 的标签包含 int 元素 id 和 String 元素 description。注解元素可用的类型如下所示:
- 所有基本类型(int、float、boolean等)
- String
- Class
- enum
- Annotation
- 以上类型的数组
如果你使用了其他类型,编译器就会报错。注意,也不允许使用任何包装类型,但是由于自动装箱的存在,这不算是什么限制。注解也可以作为元素的类型。稍后你会看到,注解嵌套是一个非常有用的技巧。
第二十四章 并发编程
- 并发是关于正确有效地控制对共享资源的访问。
- 并行是使用额外的资源来更快地产生结果。
并发通常意味着“不止一个任务正在执行中”,而并行性几乎总是意味着“不止一个任务同时执行。
并发
同时完成多个任务。在开始处理其他任务之前,当前任务不需要完成。并发解决了阻塞发生的问题。当任务无法进一步执行,直到外部环境发生变化时才会继续执行。最常见的例子是I/O,其中任务必须等待一些input(在这种情况下会被阻止)。这个问题产生在I/O密集型。
并行
同时在多个地方完成多个任务。这解决了所谓的计算密集型问题,如果将程序分成多个部分并在不同的处理器上编辑不同的部分,程序可以运行得更快。
术语混淆的原因在上面的定义中显示:其中核心是“在同一时间完成多个任务。”并行性通过多个处理器增加分布。更重要的是,两者解决了不同类型的问题:解决I/O密集型问题,并行化可能对你没有任何好处,因为问题不是整体速度,而是阻塞。并且考虑到计算力限制问题并试图在单个处理器上使用并发来解决它可能会浪费时间。两种方法都试图在更短的时间内完成更多,但它们实现加速的方式是不同的,并且取决于问题所带来的约束。
我们甚至可以尝试添加细致的粒度去定义(但是,这不是标准化的术语):
- 纯并发:任务仍然在单个CPU上运行。纯并发系统产生的结果比顺序系统更快,但如果有更多的处理器,则运行速度不会更快
- 并发-并行:使用并发技术,结果程序利用更多处理器并更快地生成结果
- 并行-并发:使用并行编程技术编写,如果只有一个处理器,结果程序仍然可以运行(Java 8 Streams就是一个很好的例子)。
- 纯并行:除非有多个处理器,否则不会运行。
Runnable 和Callable区别
https://blog.csdn.net/u012894692/article/details/80215140
execute和submit的区别与联系
https://blog.csdn.net/mryang125/article/details/81879096
附录:编程指南
附录:理解equals和hashCode方法
一个合适的 equals()函数必须满足以下五点条件:
- 反身性:对于任何 x, x.equals(x) 应该返回 true。
- 对称性:对于任何 x 和 y, x.equals(y) 应该返回 true当且仅当 y.equals(x) 返回 true 。
- 传递性:对于任何x,y,还有z,如果 x.equals(y) 返回 true 并且 y.equals(z) 返回 true,那么 x.equals(z) 应该返回 true。
- 一致性:对于任何 x和y,在对象没有被改变的情况下,多次调用 x.equals(y) 应该总是返回 true 或者false。
- 对于任何非null的x,x.equals(null)应该返回false。
下面是满足这些条件的测试,并且判断对象是否和自己相等(我们这里称呼其为右值):
- 如果右值是null,那么不相等。
- 如果右值是this,那么两个对象相等。
- 如果右值不是同一个类型或者子类,那么两个对象不相等。
- 如果所有上面的检查通过了,那么你必须决定 右值 中的哪些字段是重要的,然后比较这些字段。 Java 7 引入了 Objects 类型来帮助这个流程,这样我们能够写出更好的 equals() 函数。
存储一组元素最快的数据结构是数组
附录:对象序列化
Java 的对象序列化将那些实现了 Serializable 接口的对象转换成一个字节序列,并能够在以后将这个字节序列完全恢复为原来的对象。这一过程甚至可通过网络进行,这意味着序列化机制能自动弥补不同操作系统之间的差异。也就是说,可以在运行 Windows 系统的计算机上创建一个对象,将其序列化,通过网络将它发送给一台运行 Unix 系统的计算机,然后在那里准确地重新组装,而却不必担心数据在不同机器上的表示会不同,也不必关心宇节的顺序或者其他任何细节。
对象序列化的概念加入到语言中是为了支持两种主要特性。一是 Java 的远程方法调用(Remote Method Invocation,RMI),它使存活于其他计算机上的对象使用起来就像是存活于本机上一样。当向远程对象发送消息时,需要通过对象序列化来传输参数和返回值。
只要对象实现了 Serializable 接口(该接口仅是一个标记接口,不包括任何方法),对象的序列化处理就会非常简单。当序列化的概念被加入到语言中时,许多标准库类都发生了改变,以便具备序列化特性-其中包括所有基本数据类型的封装器、所有容器类以及许多其他的东西。甚至 Class 对象也可以被序列化。
要序列化一个对象,首先要创建某些 OutputStream 对象,然后将其封装在一个 ObjectOutputStream 对象内。这时,只需调用 writeObject() 即可将对象序列化,并将其发送给 OutputStream(对象化序列是基于字节的,因要使用 InputStream 和 OutputStream 继承层次结构)。要反向进行该过程(即将一个序列还原为一个对象),需要将一个 InputStream 封装在 ObjectInputStream 内,然后调用 readObject()。和往常一样,我们最后获得的是一个引用,它指向一个向上转型的 Object,所以必须向下转型才能直接设置它们。
try(
ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream("worm.dat"))
) {
out.writeObject("Worm storage\n");
out.writeObject(w);
}
try(
ObjectInputStream in = new ObjectInputStream(
new FileInputStream("worm.dat"))
) {
String s = (String)in.readObject();
Worm w2 = (Worm)in.readObject();
System.out.println(s + "w2 = " + w2);
}
恢复 b1 后,会调用 Blip1 默认构造器。这与恢复一个 Serializable 对象不同。对于 Serializable 对象,对象完全以它存储的二进制位为基础来构造,而不调用构造器。而对于一个 Externalizable 对象,所有普通的默认构造器都会被调用(包括在字段定义时的初始化),然后调用 readExternal() 必须注意这一点–所有默认的构造器都会被调用,才能使 Externalizable 对象产生正确的行为。
附录:流式IO
I/O 流屏蔽了实际的 I/O 设备中处理数据的细节:
- 字节流对应原生的二进制数据;
- 字符流对应字符数据,它会自动处理与本地字符集之间的转换;
- 缓冲流可以提高性能,通过减少底层 API 的调用次数来优化 I/O。
从 JDK 文档的类层次结构中可以看到,Java 类库中的 I/O 类分成了输入和输出两部分。在设计 Java 1.0 时,类库的设计者们就决定让所有与输入有关系的类都继承自 InputStream
,所有与输出有关系的类都继承自 OutputStream
。所有从 InputStream
或 Reader
派生而来的类都含有名为 read()
的基本方法,用于读取单个字节或者字节数组。同样,所有从 OutputStream
或 Writer
派生而来的类都含有名为 write()
的基本方法,用于写单个字节或者字节数组。但是,我们通常不会用到这些方法,它们之所以存在是因为别的类可以使用它们,以便提供更有用的接口。
输入流类型
InputStream
表示那些从不同数据源产生输入的类,如表 I/O-1 所示,这些数据源包括:
- 字节数组;
String
对象;- 文件;
- “管道”,工作方式与实际生活中的管道类似:从一端输入,从另一端输出;
- 一个由其它种类的流组成的序列,然后我们可以把它们汇聚成一个流;
- 其它数据源,如 Internet 连接。
每种数据源都有相应的 InputStream
子类。另外,FilterInputStream
也属于一种 InputStream
,它的作用是为“装饰器”类提供基类。其中,“装饰器”类可以把属性或有用的接口与输入流连接在一起,这个我们稍后再讨论。
表 I/O-1 InputStream
类型
类 | 功能 | 构造器参数 | 如何使用 |
---|---|---|---|
ByteArrayInputStream |
允许将内存的缓冲区当做 InputStream 使用 |
缓冲区,字节将从中取出 | 作为一种数据源:将其与 FilterInputStream 对象相连以提供有用接口 |
StringBufferInputStream |
将 String 转换成 InputStream |
字符串。底层实现实际使用 StringBuffer |
作为一种数据源:将其与 FilterInputStream 对象相连以提供有用接口 |
FileInputStream |
用于从文件中读取信息 | 字符串,表示文件名、文件或 FileDescriptor 对象 |
作为一种数据源:将其与 FilterInputStream 对象相连以提供有用接口 |
PipedInputStream |
产生用于写入相关 PipedOutputStream 的数据。实现“管道化”概念 |
PipedOutputSteam |
作为多线程中的数据源:将其与 FilterInputStream 对象相连以提供有用接口 |
SequenceInputStream |
将两个或多个 InputStream 对象转换成一个 InputStream |
两个 InputStream 对象或一个容纳 InputStream 对象的容器 Enumeration |
作为一种数据源:将其与 FilterInputStream 对象相连以提供有用接口 |
FilterInputStream |
抽象类,作为“装饰器”的接口。其中,“装饰器”为其它的 InputStream 类提供有用的功能。见表 I/O-3 |
见表 I/O-3 | 见表 I/O-3 |
输出流类型
如表 I/O-2 所示,该类别的类决定了输出所要去往的目标:字节数组(但不是 String
,当然,你也可以用字节数组自己创建)、文件或管道。
另外,FilterOutputStream
为“装饰器”类提供了一个基类,“装饰器”类把属性或者有用的接口与输出流连接了起来,这些稍后会讨论。
表 I/O-2:OutputStream
类型
类 | 功能 | 构造器参数 | 如何使用 |
---|---|---|---|
ByteArrayOutputStream |
在内存中创建缓冲区。所有送往“流”的数据都要放置在此缓冲区 | 缓冲区初始大小(可选) | 用于指定数据的目的地:将其与 FilterOutputStream 对象相连以提供有用接口 |
FileOutputStream |
用于将信息写入文件 | 字符串,表示文件名、文件或 FileDescriptor 对象 |
用于指定数据的目的地:将其与 FilterOutputStream 对象相连以提供有用接口 |
PipedOutputStream |
任何写入其中的信息都会自动作为相关 PipedInputStream 的输出。实现“管道化”概念 |
PipedInputStream |
指定用于多线程的数据的目的地:将其与 FilterOutputStream 对象相连以提供有用接口 |
FilterOutputStream |
抽象类,作为“装饰器”的接口。其中,“装饰器”为其它 OutputStream 提供有用功能。见表 I/O-4 |
见表 I/O-4 | 见表 I/O-4 |
附录:标准IO
从标准输入中读取
遵循标准 I/O 模型,Java 提供了标准输入流 System.in
、标准输出流 System.out
和标准错误流 System.err
。在本书中,你已经了解到如何使用 System.out
将数据写到标准输出。 System.out
已经预先包装0成了 PrintStream
对象。标准错误流 System.err
也预先包装为 PrintStream
对象,但是标准输入流 System.in
是原生的没有经过包装的 InputStream
。这意味着尽管可以直接使用标准输出流 System.in
和标准错误流 System.err
,但是在读取 System.in
之前必须先对其进行包装。