习题答案 http://greggordon.org/java/tij4/solutions.htm
第四章 控制流程(本节很简单,很多内容掠过)
if else
if else if
while
do - while
for
增强for循环 for( : ) (此中循环可以用于任何实现Iterable的对象)
比如for(int x : numbers)
亦可以通过自定义一个range方法返回特定的数组,比如范围限定,步长等等来控制增强for循环
与普通for循环相比增强for循环简洁易懂,所以尽量多用增强for循环
return
break continue
while(true)等价于for(;;)都可以用来执行无限循环
java的goto:goto名声似乎不怎么好,在Java中也有类似功能的东西:标签,但是,实际几年开发中我一次也没有看到有人使用这种方式编程,这里就贴下书的截图,当作课外知识了解一下吧
switch case(书中的理论还不是很新其实在jdk1.7已经支持使用string作为分支判断了,但是这样做也有一定的性能缺陷,见仁见智吧)
书中的斐波那契的练习题:
public class Fibonacci {
int fib(int n) {
if(n < 2) return 1;
return (fib(n - 2) + fib(n - 1));
}
public static void main(String[] args) {
Fibonacci f = new Fibonacci();
int k = 9;
System.out.println("First " + k + " Fibonacci number(s): ");
for(int i = 0; i < k; i++)
System.out.println(f.fib(i));
}
}
这应该是书里最早的递归函数了吧
吸血鬼数字的练习题还是挺有意思的,如果对数字游戏比较感兴趣可以看一下。
第五章 初始化与清理
初始化对象–构造函数
(命名与类名相同 必须要让编译器知道调用的具体是哪个构造函数–参数的重要性)
User user = new User();
内部操作:为user实例分配空间并调用构造函数,即user实例被初始化了
每个类都会默认有一个无参构造函数,但是如果创建了其他构造函数,则没有了默认无参构造函数
例子
/* Create a class with a String field that is initialized at the point of
* definition, and another one that is initialized by the constructor. What is
* the difference between the two approaches.
*/
class Tester2 {
String s1;
String s2 = "hello";
String s3;
Tester2() { s3 = "good-bye"; }
}
public class ConstructorTest2 {
public static void main(String[] args) {
Tester2 t = new Tester2();
System.out.println("t.s1: " + t.s1);
System.out.println("t.s2: " + t.s2);
System.out.println("t.s3: " + t.s3);
}
}
s2和s3的初始化时机不同,s2的创建与初始化是早于构造函数的,如果你想每次初始化都赋值同一个值,可以使用s2的赋值方式,但是如果你有多个构造函数,初始化时又想给域赋予不同的初始值,那么还是采用s3的赋值方式,因为构造函数中的赋值会覆盖之前的赋值。
方法重载
方法名相同 参数不同的方法就是方法的重载了。重点在于参数不同上,何为参数不同?1.参数类型 2.参数个数3.参数顺序都相同则是参数相同了,反之则不同。
1.关于顺序 但是使用参数顺序来进行重载不利于方法的识别度,代码维护不太方便
2.关于类型 类型也不行 为什么呢 因为java基本数据的类型提升
在第三章“类型提升和强制转换”一节中 我们提到,Java的基本类型存在类型提升的例子,比如
static void getInt(int x){
System.out.println(x);
}
public static void main(String[] args) {
char a = 'a';
getInt(a);
byte b = 12;
getInt(b);
}
像上面这样的char和byte就自动进行了类型提升变成了int类型,所以虽然方法定义规定参数是int类型,但实际使用是如果参数不是int类型也可能不会报错,这可能与预期不符
不过排除这个例外,用参数类型来区分方法的重载还是可靠的。
3.关于个数
参数个数不同,编译是一定可以区分重载方法的。
4.返回值类型不能区分重载方法
比如
void f(){}
int f(){return 0;}
如果你不执行int x = f();而是之间调用了f();编译器是不知道你实际想调用的是哪个方法的
默认构造器
如果没有写构造方法,Java会自动创建一个无参默认构造方法,但是如果写了,则默认构造方法就不存在了
this 关键字
this表示调用当前方法的那个对象的引用
在构造器中调用构造器
可以使用this在构造器中调用构造器,避免代码重复,此时调用必须放在第一行否则报错。
static关键字
static就是没有this的方法,可以理解为找不到调用static方法的“当前对象”,静态方法中无法调用非静态方法(有特殊例子,但感觉没有考虑的必要,喜欢钻研的可以考虑一下,不过有些钻牛角尖了)反过来却可以,静态方法设计出来就是为了能不用创建实例,只用类名就可以调用的方法。static的另一个名字叫类方法,也可以帮助理解
清理:终结处理和垃圾回收
为什么垃圾回收不是必然执行
Java垃圾回收器不会主动回收不是使用new创建的对象(比如通过C C++和JNI来创建的对象),finalize方法专门回收这类特殊对象。
Java没有析构函数,Java中对象可能不会被回收,垃圾回收不等于析构。
垃圾回收之所以不是必然执行,是因为可能系统内存始终够用,没有濒临存储空间用完,并且可能持续到程序运行完毕,随着程序退出,所有内存才会归还给系统。因为垃圾回收也是需要开销的,如果不使用他,就可以省下这笔开销了。
所以即使在finalize方法回收内存,但是垃圾回收一直没有执行,内存也就一直没有释放
finalize的正真用途
它不是用来回收普通对象的方法,而是用来回收使用native方法(C C++)创建的对象,这类对象往往使用C malloc来分配内存
对比Java的垃圾回收与C++的析构函数和对象回收
finalize的另外的用途
public class Book {
private boolean checkedOut = false;
public Book(boolean checkedOut) {
this.checkedOut = checkedOut;
}
void checkedIn(){
this.checkedOut = false;
}
@Override
protected void finalize() throws Throwable {
if (checkedOut) {
System.out.println("errorxx");
super.finalize();
}
}
public static void main(String [] args){
new Book(true);
System.gc();
}
}
这里finalize方法应该是检测回对象收前的一些错误,应该类似于Effective Java中的安全网,为了在对象清理前必须满足某种状态。但我还是不太理解这种做法的作用,难道是提示程序中可能存在的错误?
垃圾回收器如何工作
C++类似于一个院子,所有对象类似于院子里的东西
几种垃圾回收原理的比较
Java类似于一个传送带,所有对象类似于传送带上的货物,但是这个比喻有些不恰当,这涉及页面调度和资源访问
**引用计数:**对对象进行计数,没有一个引用指向对象,计数+1,当对象计数为0时表示对象可以回收。但是如果存在循环引用,则对象永远无法回收。(个人觉得此处使用离散数学的图的概念可以解决这个问题,当存在不可达的子图时,子图可以被回收,书中所述的“更快的模式”可能就是利用了图的概念的垃圾回收)
停止-复制(stop-and-copy)
先暂停程序的运行,将存活的对象copy到新堆,此时他们相互紧挨着,copy完毕后,可以想象有一个地址映射表,将所有旧地址转换成新地址。(旧数据所在的堆空间此时可以完全回收了)
此种方案有两个问题,1 空间消耗double,2 copy如果发生在没有多少垃圾时就得不偿失了
解决1:按需从堆中分配几块较大内存,copy只发生在这些大块内存之间
解决2:加入垃圾检查机制,如果没有产生多少垃圾,则切换模式,进入标记-清扫(mark-and-clean)模式
标记-清扫
从堆栈和静态存储出发,遍历引用,找出并标记所有存活引用。全部标记完毕后,清理没有被标记的对象。清理完毕后,堆空间不连续。将空间变成连续空间的工作就交给垃圾回收器了。
所以Java垃圾回收是“自适应的”,如果虚拟机检测到垃圾比较少就切换到标记-清扫模式,当发现堆空间出现许多碎片,页面调度效率降低时进入停止-复制模式。
加载机制
我们知道java文件需要先编译成class文件 让后转换为字节码装入内存。这个过程可以分为两种机制
1.一次性装载,即一次性编译所有代码
2.即时装载,即运行时只加载需要的代码
成员初始化
Java变量像C一样遵循先定义后使用的原则
类的字段可以不进行初始化,它们有默认值
方法体中的变量则必须进行初始化,否则报错,提示需要进行初始化
构造器初始化
类字段初始化
类字段存在默认初始化且顺序优先于构造器,无法阻止类的字段变量的初始化。
静态数据初始化
static数据只占用一份存储区域,static关键字不能应用于局部变量
例子
public class Bowl {
public Bowl(int marker) {
System.out.println("Bowl()"+marker);
}
void f1(int marker){
System.out.println("f1()"+marker);
}
}
public class Table {
static Bowl bowl1 = new Bowl(1);
static Bowl bowl2 = new Bowl(2);
public Table() {
System.out.println("Table()");
bowl2.f1(1);
}
void f2(int marker){
System.out.println("f2()"+marker);
}
}
public class Cupboard {
Bowl bowl3 = new Bowl(3);
static Bowl bowl4 = new Bowl(4);
static Bowl bowl5 = new Bowl(5);
public Cupboard() {
System.out.println("Cupbooard()");
bowl4.f1(2);
}
void f3(int marker){
System.out.println("f3()"+marker);
}
}
public class StaticInitialization {
static Table table = new Table();
static Cupboard cupbooard = new Cupboard();
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();
table.f2(1);
cupbooard.f3(1);
}
}
输出
Bowl()1
Bowl()2
Table()
f1()1
Bowl()4
Bowl()5
Bowl()3
Cupbooard()
f1()2
Creating new Cupboard in main
Bowl()3
Cupbooard()
f1()2
Creating new Cupboard in main
Bowl()3
Cupbooard()
f1()2
f2()1
f3()1
分析:
当使用到相关类时,静态变量最先初始化(没有使用到 则其静态变量不会初始化)且只初始化一次,接着初始化非静态类的字段,然后构造方法调用。
顺序:
class文件加载
静态变量初始化(仅仅一次)
分配内存空间
非静态的类的字段初始化
构造函数被调用
静态代码块
此类方法经常用于进行变量初始化
结合例子更易理解
public class Mug {
public Mug(int marker) {
System.out.println("Mug()"+marker);
}
void f(int marker){
System.out.println("f()"+marker);
}
}
public class Mugs {
Mug mug1;
Mug mug2;
{
mug1 = new Mug(1);
mug2 = new Mug(2);
System.out.println("Mug1 and Mug2 initialized");
}
public Mugs() {
System.out.println("Mugs()");
}
public Mugs(int i) {
System.out.println("Mugs(int)"+i);
}
public static void main(String[] args) {
System.out.println("inside main");
new Mugs();
System.out.println("new Mugs() completed");
new Mugs(1);
System.out.println("new Mugs(1) completed");
}
}
输出
inside main
Mug()1
Mug()2
Mug1 and Mug2 initialized
Mugs()
new Mugs() completed
Mug()1
Mug()2
Mug1 and Mug2 initialized
Mugs(int)1
new Mugs(1) completed
注意和静态变量初始化的例子进行对比执行顺序和执行次数是重点
静态代码块只要类的构造函数调用一次就会执行一次
数组初始化
初始化可以有如下几种形式
int a1[] = { 1, 2, 3, 4 };
Integer[] a2 = { new Integer(1), new Integer(2), 3 };
Integer[] a3 = new Integer[] { new Integer(1), new Integer(2), 3 };
可变参数
用法很简单便利,但是要注意写法吗,错误的写法可能导致空指针异常,同时在需要注重性能时,要慎用,参见EJ第42条 慎用可变参数
枚举类型简介
相对于int string常量,枚举更安全,代码的可读性更高
第六章 访问权限控制
访问修饰符的意义
1.内部重构,不影响外部。(sdk代码重构不影响客户端代码)
2.让客户端只需要关注他需要关注的部分。
命名冲突的解决方案
1.使用package
2.使用类名时进行导包
导包有两种方式
1.import例如 import java.util.ArrayList;
2.写出全称 例如 java.util.ArrayList arrayList;
Java的类的查找
Java解释器会读取classPatch的内容 将import的后面的东西翻译成路径(比如com.test会翻译成类似com/test) 然后就能读取到文件
代码简化
书中提到一种简化方法调用的一种手段比如将System.out.println封装一下可以直接写print
例如
package com.test;
public class Print {
public static void print(Object o){
System.out.println(o);
}
}
使用
package test;
import com.test.Print;
public class MyTest {
public static void main(String[] args) {
Print.print("a");
}
}
但是个人认为这样对原生方法进行二次封装,使得共同认识降低,也就是一个没有参与过此项目的人来看代码,需要追踪代码才能知道是对原生方法进行了二次封装,这种方法不多还行,多了的话,熟悉起来比较困难且如果不同项目命名不同,感觉弊大于利。
通过切换import改变行为
比如将包分为debug和非debug版本
例子
package debug;
public class Debug {
public static void debug(String s) {
System.out.println(s);
}
}
package debugoff;
public class Debug {
public static void debug(String s) {
}
}
package test;
//import debug.Debug;
import debugoff.Debug;
public class Test {
public static void main(String[] args) throws Exception {
Debug.debug("this is debug log");
}
}
通过切换不同包的Debug类可以实现Debug和正式版之间的切换
访问权限的获取
访问权限这块比较简单,略过
一种习惯
书中建议方法按照访问权限由大到小进行排序也就是public protected 默认权限 private
类访问权限
一个类至多有一个public类 但是也可以没有public类(public可以不写 此时该类为包内可用)