《Head First Java》学习笔记
对象
- 创建对象时,它会被存放在称为堆的内存区域中,此区域并非普通的堆,它是可回收垃圾的堆。
- 没有对象变量这样的东西存在,只有引用到对象的变量,它并不是对象的容器(因为对象都是保存在堆里),而是类似指向对象的指针,或者可以说是地址。
Dog myDog = new Dog();
这样一个语句,其实包含了三个步骤:Dog myDog
声明了一个引用变量,java 虚拟机会分配空间给引用变量。new Dog()
创建一个对象,java 虚拟机会分配堆空间给新建立的 Dog 对象。=
将新的 Dog 对象与引用变量连接起来了。
- 对于任意一个 java 虚拟机来说,所有引用变量的大小都一样,不管它实际上所引用的对象大小。但是,不同的 java 虚拟机间可能会以不同的方式来表示引用,因此某个 java 虚拟机的引用大小可能会大于或者小于另一个 java 虚拟机的引用变量。
- 一个引用变量如果被标记成 final 的,一旦被指派给某个对象了,就被固定下来了,不能再指派除了这个对象以外的对象给这个引用变量了。
- 数组是一个对象,不管里面放的是基础数据类型,还是对象引用。
- 实例变量(类的属性字段)默认值:
- int:0
- float:0.0
- boolean:false
- 引用:null
- 局部变量(方法中定义的变量)没有默认值,如果局部变量没有初始化就要使用的话,编译器会显示错误。
极限编程
- 多次经常性的小规模发布
- 避免加入规格没有的功能,不管“未来”会用到的功能性有多诱人
- 先写测试用的程序
- 正常工作上下班
- 随时随地重构,也就是改善程序代码
- 保持简单
- 双双结伴进行工作,并经常交换合作伙伴,以便让大家都清楚全局
多态
- 如果声明一个抽象的方法,就必须将类也标记为抽象的,不能在非抽象类中拥有抽象方法。就算只有一个抽象的方法,此类也要标记为抽象的。
- 抽象的方法没有内容,它只是为了标记处多态而存在。它的声明以分号结束。
- 抽象类可以带有抽象和非抽象的方法。
- 接口的方法带有
public
和abstract
的意义,它们一定是抽象的,没有内容的。 - extend 只能一个,implement 可以有好多个。
构造器与垃圾回收器
- 在 java 中,程序员会在乎内存中的两种区域:
- 对象的生存空间堆(heap),又称为可垃圾回收的堆
- 方法调用及变量的生存空间 栈(stack)
- 因为对象是存在于堆上,所以实例变量存在于对象所属的堆空间上。实例变量又分两种情况:
- 基础数据类型的实例变量:实例变量的值是存放在对象中,java 会依据数据类型为实例变量留下空间。
- 引用的实例变量:java 只会为包含这个引用的对象留下引用所用到的空间,而不是这个引用的对象所用的空间。而引用的对象要等到这个引用被赋值的时候,java 才会给在堆上给这个引用的对象分配空间。
- 编译器只会在你完全没有写构造函数时才会帮你建一个无参的构造函数。如下:
public ClassName(){ super(); }
当你已经写了一个有参数的构造函数,但是又需要一个无参的构造函数时,你就要自己手动写一个无参的构造函数。 - 如果你有构造函数但没有调用
super()
,编译器会帮你对每个重载版本的构造函数加上super()
。编译器帮忙加的一定会是没有参数的版本,即使父类有多个重载版本,也只有无参数的这个版本会被调用到。super()
必须是构造函数的第一个语句。 - 使用
this()
来从某个构造函数调用同一个类的另一个构造函数。this()
只能用在构造函数中,且必须是第一行语句。所以,super()
和this()
不能兼得。 - 对象的生命周期完全要看引用到它的“引用”。如果引用还活着,则对象也会继续活在堆上。如果引用死了,对象也就跟着消亡了。
- 局部变量只会存活在声明该变量的方法中。实例变量的寿命与对象相同。如果对象还活着,则实例变量也会活着。
- life 与 scope 的区别:只要变量的堆栈块还存在于堆栈上,变量就还活着。但是当此方法调用别的方法时,该变量虽然还活着,但是超出了变量的使用范围。
- 有 3 种方法释放对象的引用:
- 引用永久性地离开它的范围,比如一个方法执行结束了,那在这个方法内声明的引用变量就消失了,引用的对象就会变成可回收的了。
- 引用被赋值到其他对象上。
- 直接将引用设定为 null。
静态
- 静态方法不能调用非静态的变量和方法。
- 静态变量:被同类的所有实例共享的变量,是在类被加载时初始化的,会在该类的任何对象创建之前、在该类的任何静态方法执行之前初始化的。
- 静态变量值必须在声明或者静态初始化程序中赋值。
- 通常,java 虚拟机会加载某个类,是因为第一次有人尝试要创建该类的实例,或是使用该类的静态方法或变量。
- 关于 final:
- final 的变量代表不能改变它的值。
- final 的方法代表不能覆盖掉该方法。
- final 的类代表不能继承该类。
- 如果类只有静态的方法,可以将该类的构造函数标记为 private 的以避免被初始化。
- 在 java 中的常量是把变量同时标记为 static 和 final 的。
- Boolean 没有
parseBoolean()
方法,但是 Boolean 的构造函数可以取用 String 来创建对象,如:boolean b = new Boolean("true").booleanValue();
。 - format 格式化说明:
%[argument number][flags][width][.precision]type
,其中[]
里面的都是可选项目,只有%
和type
是必要的。argument number
:如果要格式化的参数超过一个以上,可以在这里指定是哪一个。flags
:特定类型的特定选项,例如数字要加逗号或者正负号。width
:最小的字符数,输出可以超过此宽度,若不足则会自动补零。.precision
:精确度,注意前面有个圆点符号。
例如,format("%,6.1f", 42.000)
中,%,6.1f
的,
是flags
,6
是width
,.1f
是.precision
<
这个符号是个特殊的指示,用来告诉格式化程序重复利用之前用过的参数。例如:String.format("%tA, %tB %td", today, today, today);
可改成:String.format("%tA, %<tB %<td", today);
- 要取用当前的日期时间就用 Date,其余功能可以从 Calendar 上面找。
异常
- 编译器不会注意 RuntimeException 类型的异常。因为大部分的 RuntimeException 都是因为程序逻辑的问题,try/catch 是用来处理真正的异常,而不是程序的逻辑错误。
- 有多个 catch 块时要从小排到大。
序列化
- 序列化:object -> ObjectOutputStream(对象被碾平) -> FileOutputStream(对象被当做字节处理)-> 文件
- 反序列化:文件 -> FileOutputStream(对象被当做字节读入)-> ObjectInputStream(加载类,加载实例变量的存储值)-> 对象
- 序列化程序会将对象版图上的所有东西存储起来。被对象的实例变量所引用的所有对象都会被序列化。
- Serializable 是标记用接口,并没有任何方法需要实现的,它的唯一目的就是声明有实现它的类是可以被序列化的。
- 整个对象版图都必须正确地序列化,不然就得全部失败。如果需要序列化的对象中有变量不能被序列化,则这个对象就会序列化失败。如果某实例变量不能或者不应该被序列化,就把它标记为 transient 的。标记为 transient 的变量在恢复的时候是 null(对对象应用而已)或者基础数据类型的默认值。
- 如果两个要序列化的对象都有引用实例变量指向相同的对象,那么只有一个对象会被存储,其他引用会复原指向该对象。
- 静态变量不会被序列化。因为静态变量代表“每个类一个”而不是“每个对象一个”。所以当对象被还原时,静态变量会维持类中原本的样子,而不是对象存储时的样子。
网络与线程
- TCP 端口是个 16 位的值。从 0
1023 的 TCP 端口号是保留给已知的特定服务使用,从 102465535 可以根据需要来使用。 - 一旦建立了连接,客户端可以从 socket 取得底层串流:
sock.getInputStream()
- InputStreamReader 是个转换字节成字符的桥梁。它主要用来链接 BufferedReader 与底层的 Socket 输入串流。
InputStreamReader stream = new InputStreamReader(sock.getInputStream());
- BufferedReader 链接 InputStreamReader 与来自 Socket 的输入串流以读取服务器的文本数据。
BufferedReader reader = new BufferedReader(stream);
- PrintWriter 直接链接 Socket 输出串流,可直接调用
print()
和println()
输出字符串给服务器。PrintWriter writer = new PrintWriter(sock.getOutputStream());
writer.println("message to send");
- Thread 是工人,Runnable 是这个工人的工作。
- 当创建了一个 Thread 对象 t,执行
t.start()
只是让它变成可执行的状态,什么时候真正执行,是 java 虚拟机的线程调度机制来决定的。一旦线程进入可执行状态,它会在可执行与执行中两种状态来来去去,同时也有另一种状态:暂时不可执行(又称为被堵塞状态) - 线程调度器会决定哪个线程从等待状态中被挑出来运行,以及何时把哪个线程送回等待被执行的状态。它会决定某个线程要运行多久,当线程被踢出时,调度器也会指定线程要回去等待下一个机会或者是暂时地堵塞。调度器在不同的 java 虚拟机上面有不同的做法。
- 每个对象有一个锁,每个锁只有一把钥匙。通常对象时没有上锁的,当对象有一个或者多个同步化的方法时,线程只有在取得对象锁的钥匙才能进入同步化的方法。
- 使用 synchronized 关键词来修饰方法,使它每次只能被单一的线程存取。要保护数据,就把作用在数据上的方法给同步化。所以线程执行时遇上同步化的方法,会认知到它需要对象的钥匙才可以进入该方法。它会取得钥匙(这是由 java 虚拟机来处理的),如果可以拿到钥匙,才可以进入方法,并在完成同步化方法以后放开钥匙。当线程持有钥匙时,没有其他线程可以进入该对象的同步化方法,因为每个对象只有一个钥匙。
- 同步化的方法有一些额外的成本:
- 查询钥匙等性能上的损耗
- 同步化的方法会让程序因为要同步并行而慢下来
- 可能会导致死锁现象
原则上应该只做最少量的同步化,可用 synchronized 来修饰一行或者数行的指令而不必整个方法都同步化。
- 每个被载入的类也是有锁的。所以如果有 3 个 Dog 对象在堆上,则表示有 4 个与 Dog 有关的锁。3 个是 Dog 实例的,一个是 Dog 类的。当你要对静态方法做同步化时,java 会使用类本身的锁。
数据结构
- 使用
Collections.sort()
方法进行排序的对象需要实现Comparable
接口,或者使用Comparator
。如果传Comparator
给sort()
方法,则排序是由Comparator
而不是对象的compareTo()
方法来决定。
其他
&
和|
通常会用来做位运算,但是用在 boolean 表达式时,会强制 java 虚拟机一定要计算运算符两边的算式。- 使用 import 只是帮你省下每个类前面的包名称而已,程序不会因为用了 import 而变大或变慢。
- 像
java.lang
,它是一个预先被引用的包,因为java.lang
是个经常会用到的基础包,所以可以不必 import,java.lang.String
与java.lang.System
是独一无二的 class,java 会知道要去哪里找。 - 内部类可以使用外部所有的方法与变量,就算私用的也一样。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Annie's Blog!