Java编程思想(第四版)学习笔记 第一章---第五章
趁着现在晚上还有点自己的时间,将被遗忘在角落里的这本经典书籍拿出来学习一下,使用博客来记录下来自己容易忽略的知识点,以便日后提醒自己一下。
第一章:对象导论
1.1抽象过程
类:类描述了具有相同特(数据元素)和行为(功能)的对象集合。所以一个类实际上就是一个数据类型,例如浮点型数字具有形同的特性和行为集合。
1. 2访问控制
三个关键字: public
、private
、protected
这些访问控制指定词,决定了紧跟在其后被定义的东西可以被谁使用。
public : 表示紧随其后的元素对任何人都是可用的。
private :表示出了类型创建者和类型的内部方法之外的任何人都不能访问的元素 。private就像你与客户端程序员之间的一堵砖墙,如果有人视图访问private成员,就是在编译时得到错误的信息。
protected : 与private作用相当,差别仅在于继承的类可以访问protected成员,但是不能访问private成员。
还有一种默认的访问权限,当没有使用前面提到的任何访问指定词时,它将发挥作用。这种权限通常被称为包访问权限,因为在这种权限下,类可以访问在同一个包(库构件)中的其他类的成员,但是在包之外,这些成员如同制定了private一样。
访问控制的存在原因:
第一:让客户端程序员无法触及他们不应该触及的部分,这些部分对数据类型的内部操作来说是必须的,但是并不是用户解决待定问题所需的接口的一部分。
第二:允许库设计者可以改变类内部的工作方式而不用担心会影响到客户端程序员。
第二章:一切都是对象
2. 1用引用操纵对象
尽管一切都看作对象,但操纵的标识符实际上是对象的一个“引用”(reference)。
可以将这一情形想象成用遥控器(引用)来操纵电视机(对象)。只要握住遥控器,就能保持与电视机的连接。当有人想改变频道或者减小音量时,实际操控的是遥控器(引用),再由遥控器来调控电视机(对象)。如果想在房间里四处走走,同时仍能调控电视机,那么只需要携带遥控器(引用),而不是电视机(对象)。
此外,即使没有电视机,遥控器亦可以独立存在。也就是说,你拥有一个引用,并不一定需要有一个对象与它关关联。
因此,如果想操纵一个词或者局子,则可以创建一个String引用: String s;
这里创建的只是引用,而不是对象。如果此时向s发送一个消息,就会返回一个运行时错误。这是因为此时实际上s没有与任何事物相关联(即,没有电视机)。
因此一种安全的做法是:创建一个引用的同时便进行初始化。String s = "asdf";
一旦创建了一个引用,就希望它能与一个新的对象相关联。通常使用new
操作符来实现这一目的。
new关键字的意思是“给我一个新对象”,所以上面的例子可以写成:String s = new String("asdf");
2. 2基本类型
其中基本类型
有九种,基本数据类型
有八种,另外一种基本类型是void。
基本类型 | 大小 | 最小值 | 最大值 | 包装类型 |
---|---|---|---|---|
boolean | - | - | - | Boolean |
char | 16-bit | Unicode 0 | Unicode 2^16-1 | Character |
byte | 8 bit | -128 | +127 | Byte |
short | 16 bit | -2^15 | +2^15-1 | Short |
int | 32 bit | -2^31 | +2^31-1 | Integer |
long | 64 bit | -2^63 | +2^63-1 | Long |
float | 32 bit |
IEEE 754 | IEEE754 | Float |
double | 64 bit |
IEE754 | IEE754 | Double |
void | - | - | - | Void |
注:IEEE754表示浮点数。所有的数值类型都有正负号,不要去去寻找无符号的数值类型。
Boolean类型所占存储空间大小没有明确的规定,仅定义为能够取字面值true或false。
高精度数字:BigInteger和BigDecimal,没有对应的基本类型。
BigInteger
:支持任意精度的整数。也就是说可以准确地表示任何大小的整数值,不会丢失任何信息。
BigDecimal
:支持任何精度的定点数,例如可以进行精确的货币计算。
第三章:操作符
3.7.1测试对象的等价性
关系操作符生成的是一个Boolean类型的结果,其中==和!=也同大于小于等符号一样,适用于所有的对象。
但是对于第一次接触Java的程序员来说这里有一点会感到迷惑的地方。下面是一个例子:
public class Test {
public static void main(String[] args) {
Integer n1 = new Integer(47);
Integer n2 = new Integer(47);
System.out.println(n1 == n2);
System.out.println(n1 != n2);
}
}
/* Output: false
true*///:~
语句System.out.println(n1 == n2);
将打印出括号内的比价的布尔值的结果。读者可能认为输出的结果肯定先是true再是false,因为两个Integer对象都是相同的。但是尽管对象的内容是相同的,然而对象的引用确是不相同的,而==和!= 比较的就是对象的引用
。所以输出结果实际上先是false再是true。
如果想比较两个对象的实际内容是否相同,此时必须使用所有对象都使用的特殊的方法equals()。但是这个方法不适用于基本类型,因为基本类型直接使用==和!=即可。
public class EqualsMethod {
public static void main(String[] args) {
Integer n1 = new Integer(47);
Integer n2 = new Integer(47);
System.out.println(n1.equals(n2));
}
}
/* Output: true*///:~
结果正如我们预料的那样,但事情并不总是那么简单,假设你创建了自己的类,就像下面这样:
class Value{
int i;
}
public class EqualsMethod2 {
public static void main(String[] args) {
Value v1 = new Value();
Value v2 = new Value();
v1.i = v2.i = 100;
System.out.println(v1.equals(v2));
}
}
/* Output: false*/
事情再次令人费解了:结果又是false!这是由于equals()的默认行为是比较引用
。所以除非在自己的新类中覆盖equals()方法,否则不可能表现出我们希望的行为。
第四章:控制执行流程
4.1 switch
switch语句是实现多路选择(也就是说从一系列执行路径中挑选一个)的一种干净利落的方法。但它要求使用一个选择因子
,并且必须是int或char那样的整数值
。例如,假若将一个字符串或者浮点数作为选择因子使用,那么它们在switch语句里是不会工作的。对于非整数类型则必须使用一系列if语句。另外在case语句中,使用单引号引起的字符也会产生用于比较的的整数值
。
Random rand = new Random(47);
// 产生一个0-26之间的一个值,然后加上一个偏移量‘a',即可随机产生小写字符
int c = rand.nextInt(26) + 'a';
switch (c){
case 'a' :
System.out.println("a"); break;
case 'e':
System.out.println("e"); break;
default:
System.out.println("default"); break;
}
在上面的定义中,大家会注意到每个case均以一个break结尾,这样可是执行流程跳转至switch主体的末尾。这是构建一种传统的switch语句,但break是可选的。若省略break会继续执行后面的case语句,直到遇到一个break为止。
第五章:初始化与清理
5.2方法重载
- 重载:简单说,就是函数或者方法有相同的名称,但是参数列表不相同的情形,这样的同名不同参数的函数或者方法之间,互相称之为重载函数或者方法。(重载)
- 区分重载方法:每个重载的方法都必须有一个独一无二的参数类型列表。甚至参数顺序的不同也足以区分两个方法(一般不这么用,会使代码难以维护)。
- 设计基本类型的重载:基本类型能从一个较小的类型自动提升至一个较大的类型,此过程涉及到重载可能会造成混淆。
package chart5;
public class PrimitiveOverloading {
// boolean can't be automatically converted
static void prt(String s) {
System.out.println(s);
}
void f1(char x) { prt("f1(char)"); }
void f1(byte x) { prt("f1(byte)"); }
void f1(short x) { prt("f1(short)"); }
void f1(int x) { prt("f1(int)"); }
void f1(long x) { prt("f1(long)"); }
void f1(float x) { prt("f1(float)"); }
void f1(double x) { prt("f1(double)"); }
void f2(byte x) { prt("f2(byte)"); }
void f2(short x) { prt("f2(short)"); }
void f2(int x) { prt("f2(int)"); }
void f2(long x) { prt("f2(long)"); }
void f2(float x) { prt("f2(float)"); }
void f2(double x) { prt("f2(double)"); }
void f3(short x) { prt("f3(short)"); }
void f3(int x) { prt("f3(int)"); }
void f3(long x) { prt("f3(long)"); }
void f3(float x) { prt("f3(float)"); }
void f3(double x) { prt("f3(double)"); }
void f4(int x) { prt("f4(int)"); }
void f4(long x) { prt("f4(long)"); }
void f4(float x) { prt("f4(float)"); }
void f4(double x) { prt("f4(double)"); }
void f5(long x) { prt("f5(long)"); }
void f5(float x) { prt("f5(float)"); }
void f5(double x) { prt("f5(double)"); }
void f6(float x) { prt("f6(float)"); }
void f6(double x) { prt("f6(double)"); }
void f7(double x) { prt("f7(double)"); }
void testConstVal() {
prt("Testing with 5");
f1(5); f2(5); f3(5); f4(5); f5(5); f6(5); f7(5);
}
void testChar() {
char x = 'x';
prt("char argument:");
f1(x); f2(x); f3(x); f4(x); f5(x); f6(x); f7(x);
}
void testByte() {
byte x = 0;
prt("byte argument:");
f1(x); f2(x); f3(x); f4(x); f5(x); f6(x); f7(x);
}
void testShort() {
short x = 0;
prt("short argument:");
f1(x); f2(x); f3(x); f4(x); f5(x); f6(x); f7(x);
}
void testInt() {
int x = 0;
prt("int argument:");
f1(x); f2(x); f3(x); f4(x); f5(x); f6(x); f7(x);
}
void testLong() {
long x = 0;
prt("long argument:");
f1(x); f2(x); f3(x); f4(x); f5(x); f6(x); f7(x);
}
void testFloat() {
float x = 0;
prt("float argument:");
f1(x); f2(x); f3(x); f4(x); f5(x); f6(x); f7(x);
}
void testDouble() {
double x = 0;
prt("double argument:");
f1(x); f2(x); f3(x); f4(x); f5(x); f6(x); f7(x);
}
public static void main(String[] args) {
PrimitiveOverloading p = new PrimitiveOverloading();
p.testConstVal();
p.testChar();
p.testByte();
p.testShort();
p.testInt();
p.testLong();
p.testFloat();
p.testDouble();
}
}
/*
Testing with 5
f1(int) f2(int) f3(int) f4(int) f5(long) f6(float) f7(double)
char argument:
f1(char) f2(int) f3(int) f4(int) f5(long) f6(float) f7(double)
byte argument:
f1(byte) f2(byte) f3(short) f4(int) f5(long) f6(float) f7(double)
short argument:
f1(short) f2(short) f3(short) f4(int) f5(long) f6(float) f7(double)
int argument:
f1(int) f2(int) f3(int) f4(int) f5(long) f6(float) f7(double)
long argument:
f1(long) f2(long) f3(long) f4(long) f5(long) f6(float) f7(double)
float argument:
f1(float) f2(float) f3(float) f4(float) f5(float) f6(float) f7(double)
double argument:
f1(double) f2(double) f3(double) f4(double) f5(double) f6(double) f7(double)
*/
可以看到常数值5被当做int值处理,所以如果有某个重载方法接受int型参数,它就会被调用。如果传入的参数类型(实际参数类型)小于方法中声明的形式参数类型,实际数据类型就会被提升。char类型略有不同,如果无法找到恰好接受char参数的方法,就会把char直接提升至int型。
如果传入的实际参数大于重载方法声明的形式参数,就得通过实际类型转换类执行窄化转换,否则编译器会报错。
根据方法的返回值来区分重载方法是行不通的。
5.4 this关键字
- this关键字
只能在方法内部使用,表示“调用方法的那个对象”的引用。this的用法和其他对象引用并无不同。需要注意如果在方法内部调用同一个类的另一个方法,就不必使用this,直接调用即可。当前方法中的this引用会自动应用于同一类中的其他方法。只能在方法内部使用,表示“调用方法的那个对象”的引用。this的用法和其他对象引用并无不同。需要注意如果在方法内部调用同一个类的另一个方法,就不必使用this,直接调用即可。当前方法中的this引用会自动应用于同一类中的其他方法。
public class Apricot(){
void pick(){/* */ }
void pit(){pick(); /* */}
}
- 在构造器中调用构造器
可以用this在一个构造器中调用另一个构造器,但是只能调用一个。必须将构造器调用置于最起始处,否则编译器会报错。 - static的含义
static(静态)方法就是没有this的方法。在static方法的内部不能调用非静态方法,反过来倒是可以。而且可以在没有创建任何对象的前提下,仅仅通过类本身来调用static方法。它很像全局方法。
5.5 清理:中级处理和垃圾回收
Java有垃圾回收器负责回收无用对象占据的内存资源。
Java允许在类中定义一个名为finallize()的方法。它的工作原理“假定”是这样的:一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用其finallize(0方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。
Java里的对象并非总是被垃圾回收
。换句话说:
- 对象可能不被垃圾回收。
- 垃圾回收不等于析构(C++中销毁对象必须要用到的函数)。
- 垃圾回收只与内存有关。
随着程序的退出,哪些资源也会全部交还给操作系统。
无论是“垃圾回收”还是“终结”,都不保证一定会发生。如果Java虚拟机(JVM)并未面临内存耗尽的情形,它是不会浪费时间去执行垃圾回收以恢复内存的。
5.7 构造器初始化
- 初始化顺序
在类的内部,变量定义的先后顺序决定了初始化的顺序。即使变量定义三部于方法定义之间,它们仍然会在任何方法(包括构造器)被调用之前得到初始化。
class Tag {
Tag(int marker) {
System.out.println("Tag(" + marker + ")");
}
}
class Card {
Tag t1 = new Tag(1); // Before constructor
Card() { // Indicate we're in the constructor:
System.out.println("Card()");
t3 = new Tag(33); // Re-initialize t3
}
Tag t2 = new Tag(2); // After constructor
void f() {
System.out.println("f()");
}
Tag t3 = new Tag(3); // At end
}
public class OrderOfInitialization {
public static void main(String[] args) {
Card t = new Card();
t.f(); // Shows that construction is done }
}
}
/* Output:
Tag(1)
Tag(2)
Tag(3)
Card()
Tag(33)
f()*/
- 静态数据的初始化
无论创建多少个对象,静态数据都只占用一份存储区域。static关键字不能应用于局部变量,因此它只能作用于域。
class Bowl {
Bowl(int marker) {
System.out.println("Bowl(" + marker + ")");
}
void f(int marker) {
System.out.println("f(" + marker + ")");
}
}
class Table {
static Bowl b1 = new Bowl(1);
Table() {
System.out.println("Table()");
b2.f(1);
}
void f2(int marker) {
System.out.println("f2(" + marker + ")");
}
static Bowl b2 = new Bowl(2);
}
class Cupboard {
Bowl b3 = new Bowl(3);
static Bowl b4 = new Bowl(4);
Cupboard() {
System.out.println("Cupboard()");
b4.f(2);
}
void f3(int marker) {
System.out.println("f3(" + marker + ")");
}
static Bowl b5 = new Bowl(5);
}
public class StaticInitialization {
public static void main(String[] args) {
System.out.println("Creating new Cupboard() in main");
new Cupboard();
System.out.println("Creating new Cupboard() in main");
new Cupboard();
t2.f2(1);
t3.f3(1);
}
static Table t2 = new Table();
static Cupboard t3 = new Cupboard();
}
/*Output:
Bowl(1)
Bowl(2)
Table()
f(1)
Bowl(4)
Bowl(5)
Bowl(3)
Cupboard()
f(2)
Creating new Cupboard() in main
Bowl(3)
Cupboard()
f(2)
Creating new Cupboard() in main
Bowl(3)
Cupboard()
f(2)
f2(1)
f3(1)*/
由输出可见,静态方法只有在必要的时刻才会进行。如果不创建Table对象,也不引用Table.b1或Table.b2,那么静态的b1和b2永远都不会被创建。只有在第一个Table对象被创建(或者第一次访问静态数据)的时候,它们才会被初始化。此后静态对象不会再次被初始化。
初始化顺序先是静态对象(如果它们尚未因前面的对象创建过程而被初始化),而后是“非静态”对象。从输出结果可看到,虽然static Table t2 = new Table(); static Cupboard t3 = new Cupboard();
在类的最下面,当main方法执行之前,这两个对象已经被初始化了,所以会在第一次输出Creating new Cupboard() in main
时输出前面一大堆。即在执行main()方法,必须加载StaticInitialization类,然后其静态域table和cupboard被初始化,这将导致他们对应的类也被加载,并且由于他们也都包含静态的Bowl对象,因此Bowl随后也被加载。
- 显示的静态初始化
Java允许将多个静态初始化动作组织成一个特殊的“静态化子句”,也叫静态块。
public class Spoon{
static int i;
static {
i = 47
}
}
尽管上面的代码看起来像一个方法,但实际上只是一段跟在static关键字后面的代码。与其他静态初始化动作一样,这段代码仅执行一次:当首次生成这个类的对象时,或者首次访问属于那个类的静态数据成员时(即便从未生成过那个类的对象)。
public class E14_StaticStringInitialization {
static String s1 = "Initialized at definition";
static String s2;
static {
s2 = "Initialized in static block";
}
public static void main(String args[]) {
System.out.println("s1 = " + s1);
System.out.println("s2 = " + s2);
}
}
/* Output:
s1 = Initialized at definition
s2 = Initialized in static block */
- 非静态实例初始化
class Mug {
Mug(int marker) {
System.out.println("Mug(" + marker + ")");
}
void f(int marker) {
System.out.println("f(" + marker + ")");
}
}
public class Mugs {
Mug c1;
Mug c2;
{
c1 = new Mug(1);
c2 = new Mug(2);
System.out.println("c1 & c2 initialized");
}
Mugs() {
System.out.println("Mugs()");
}
public static void main(String[] args) {
System.out.println("Inside main()");
Mugs x = new Mugs();
}
}
/*Output:
Inside main()
Mug(1)
Mug(2)
c1 & c2 initialized
Mugs()*/
5.8数组初始化
数组只是相同类型的、用一个标识符封装到一起的一个对象序列或基本数据类型数据序列。
数组是通过方括号下标操作符[ ]来定义和使用的。 要定义一个数组,只需要在类型名后加上一对方括号即可。
int al; // 表明类型是“一个int型数组”
编译器不允许指定数组的大小
即int[2] a = {1, 2};
这样是会报错的。int[] a = {1, 2};
这是正确的。length是数组的固有成员,通过它可知道数组内包含了多少个元素,但是不能对其修改。从第0个元素开始,最大下标是length-1。如果超出这个边界,会报ArrayIndexOutOfBoundsException
异常。
若要设置数组长度,可以这样int[ ] a1; a1 = new int[2];
表明数组a被初始化成a = {0, 0}
数组元素中的基本数据类型值会自动初始化成空值(对于数字和字符,就是0,布尔型是false)。
在Java中可以将一个数组赋值给另一个数组:int [ ] a2 = a1; 这样做的只是复制了一个引用,即a1和a2都只是一个数组引用,即是相同数组的别名,共同指向数组{0,0},操作a2里面的数值,会影响a1里的数值。
如果你创建了一个非基本类型的数组,那么你就创建了一个引用数组
。以整型的包装器类Integer为例,它是一个类而不是基本类型:
import java.util.Arrays;
import java.util.Random;
public class ArrayClassObj {
public static void main(String[] args) {
Random rand = new Random(47);
Integer[] a = new Integer[rand.nextInt(20)];
System.out.println("length of a = " + a.length);
for (int i=0; i<a.length; i++){
a[i] = rand.nextInt(500);
}
System.out.println(a.toString());
System.out.println(Arrays.toString(a));
}
}
/*Output:
length of a = 18
[Ljava.lang.Integer;@74a14482
[55, 193, 361, 461, 429, 368, 200, 22, 207, 288, 128, 51, 89, 309, 278, 498, 361, 20]
*/
这里,即使使用new创建数组之后:
Integer[] a = new Integer[rand.nextInt(20)];
它还是一个引用数组,并且直到通过创建新的Integer对象(在本例中是通过自动包装机制创建的),并把对象赋值给引用,初始化进程才算结束:
a[i] = rand.nextInt(500);
数组的大小时通过Random.nextInt()方法随机决定的,这个方法会返回0到输入参数之间的一个值。这表明数组创建确实是在运行时刻进行的。
Arrays.toString()方法属于java.util标准类库,它将产生一位数组的可打印版本。
如果忘记了创建对象,并且试图使用数组中的空引用,就会在运行时产生异常。
如果一个类没有重写Object的tostring() 方法的话,打印这个类的对象时会输出:类名称+@+十六进制数字
就是打印类的名字和对象的地址。