- Java中,Integer与Boolean是不能相容的,例如:
int x = 1;
if (x) {
...
}
是错误的。
- Java垃圾回收机制简介
- Java中primitive主数据类型
boolean Java虚拟机决定 true或false
char 16bits 0~65536
byte 8bits -128~127
short 16bits -32768~32767
int 32bits -2147483648~2147483647
long 64bits -2^63~2^63-1
float 32bits 范围规模可变,有效数字6~7位
double 64bits 范围规模可变,有效数字15位
- Java对象引用变量
Dog d = new Dog();
这个里面d就是所谓的对象引用变量。
实际上,这个d是不存在的,它只是一个引用(reference)到对象的变量。它所保存的是存取对象的方法。
它并不是对象的容器,而是类似指向对象的指针。或者可以说地址。
比较primitive主数据类型,primitive主数据类型变量是以字节来代表实际的变量值,但对象引用变量却是以字节来表示取得对象的方法。
Dog d = new Dog();
第一步,Java虚拟机为变量d分配空间;
第二步,Java虚拟机为新建立的Dog对象分配堆空间;
第三步,建立d与新建立的Dog对象之间的引用。
没有引用到任何对象的引用变量的值为null值。
对象引用变量有多大?
不知道。
但是,不管对象引用变量所引用的对象大小,所有的对象引用变量都具有相同的大小。
不能像C那样对对象引用变量进行运算。
- 传递参数英文
形参 parameter
- Java方法中参数是通过值传递的,也就是说通过拷贝传递。
对于对象引用变量,传递的也是它们所拥有的值,它们所拥有的值是存取对象的方法。
public static void t(int a) {
a++;
}
public static void main(String[] args) {
// ArrayList<Apple> apples = new ArrayList<Apple>();
int i = 0;
t(i);
System.out.println(i);
}
Output:
0
- Java成员变量与局部变量的初始值
成员变量的默认值
integers 0
floating points 0.0
booleans false
references null
局部变量没有初始值,如果在初始化就要使用的话,编译器会显示错误。
- Java开发一个具体的类流程,提供一种思考的思路
列出成员变量和方法
编写方法的伪代码
编写方法的测试程序
实现类
测试方法
除错或重新设计
- 伪代码
例如:
一个类SimpleDotCom
SimpleDotCom
int[] locationCells
int numOfHits
String checkYourself(String guess)
void setLocationCells(int[] loc)
伪代码:
DECLARE an int array to hold the location celss. Call it locationCells.
DECLARE an int to hold the number of hits. Call it numOfHits and SET it to 0.
DECLARE a checkYourself() method that takes a String for the user's guess ("1", "3", etc.), checks it, and returns a result representing a "hit", "miss", or "kill".
DECLARE a setLocationCells() setter method that takes an int array (which has the three cell locations as ints (2, 3, 4, etc.)).
METHOD: String checkYourself(String guess)
GET the user guess as a String parameter
CONVERT the user guess to an int
REPEAT with each of the location cells in the int array
//COMPARE the user guess to the location cell
IF the user guess matches
INCREMENT the number of hits
//FIND OUT if it was the last location cell
IF number of hits is 3, RETURN "kill" as the result
ELSE it was not a kill, so RETURN "hit"
END IF
ELSE the user guess did not match, so RETURN "miss"
END IF
END REPEAT
END METHOD
METHOD: void setLocationCells(int[] loc)
GET the cell locations as an int array parameter
ASSIGN the cell locations parameter to the cell locations instance variable
END METHOD
- 测试程序
先编写测试程序代码的概念来自于极限编程(XP)方法论。
XP的概念于20世纪90年代出现,它是由一组被证明有效的施行方法所组成的,这些方法都是被设计来共同运作,但许多人只选择性地实行部分的XP规则。这些方法包括了:
多次经常性的小规模发布。
避免加入规格没有的功能(不管“未来”会用到的功能性有多诱人)。
先写测试程序。
正常工作上下班。
随时随地重构(refactor),也就是改善程序代码。
保持简单。
双双结伴工作,并经常交换伴侣以便让大家都清楚全局。
先写出测试程序的好处:
思考与编写测试用的程序代码能够帮助你了解被测的程序应该要做哪些事情。
理想上,先写出一点点的测试程序,然后只编写能够通过该测试的方法就好。之后再编写一点测试程序,再编写新的实现通过测试程序。经过如此循环,你就能够保证新加入的代码不会破坏原有已经测试通过的部分。
我个人认为,伪代码提供了方法的一个逻辑思路,一个大致的蓝图。而思考测试程序的同时,能够加强对于实现时一些细节的思考。能够让我们明确实现应该做什么事情,具体怎么做,哪些细节要注意。
- 伪代码示例(一)
类SimpleDotComGame需要有下面的功能:
创建出SimpleDotCom对象
赋值给它
要求玩家猜测
检查猜测值
重复猜测直到击沉为止
显示玩家的猜测次数
METHOD: public static void main(String[] args)
DECLARE an int variable to hold the number of user guesses, named numOfGuesses. SET it to 0.
DECLARE an int variable to hold the number of user guesses, named numOfGuesses. SET it to 0.
MAKE a new SimpleDotCom instance.
COMPUTE a random number between 0 and 4 that will be the starting location cell position.
MAKE an int array with 3 ints using the randomly-generated number, that number incremented by 1, and that number incremented by 2 (example 3, 4, 5).
INVOKE the setLocationCells() method on the SimpleDotCom instance.
DECLARE a boolean variable representating the state of the game, named isAlive. SET it to true.
WHILE the dot com is still alive (isAlive == true):
GET the input from the command line.
//CHECK user guess
INVOKE checkYourself() method on the SimpleDotCom instance.
INCREMENT numOfGuesses variable.
//FIND OUT if it is killed
IF result is "kill":
SET isAlive to false (which means wo won't enter the loop again).
PRINT the number of user guesses.
END IF
END WHILE
END METHOD
从上述伪代码中,我们可以学习到以下几点:
声明一个变量或者方法用DECLARE;
给一个变量命名可以不断句用", named"来形容,也可以断句用". Call it"来形容;
为变量赋值用的是SET;
创建一个对象用MAKE,注意用instance而不是object;
调用一个方法用INVOKE,某一个实例的方法不是用of the * instance,而是on the * instance;
- 判断一个正整数是不是2的幂
n is a power of 2
- 递增递减前置与后置
int z = x++;
x = 1, z = 0.
int x = 0;
int z = ++x;
x = 1, z = 1.
- 计算机编码小知识
为了区分正数和负数,自然想到用最高位标识,0表示正数,1表示负数。
如此一来,占2个字节(16位)的short的范围为-32767~32767,存在两个0,一个是1000 0000 0000 0000,另外一个是0000 0000 0000 0000。
计算机采用的补码表示:负数由最高位的标识位和绝对值的反码加1组成。
例如:-1。
绝对值:0000 0000 0000 0001
最高位标识:1000 0000 0000 0001
绝对值部分取反:1111 1111 1111 1110
加1:1111 1111 1111 1111
通过这种编码方式,没有一个数可以表示为1000 0000 0000 0000,于是人为规定1000 0000 0000 0000为-32768(前提是当前编码系统只有16位,如果是64位那么1000 .... 0000就是-2^64)。
为什么计算机要采用补码表示?
因为计算机中只有加法器,而补码表示法,在进行减法运算(x-y)时,只需对y取补,然后两者相加即可。
同时,我们可以看到在short中运算32767-32768时,实际计算过程为:
0111 1111 1111 1111 + 1000 0000 0000 0000 = 1111 1111 1111 1111
也就是-1。
- Java中import不同于C中include
滥用include会使得文件很大。
import不像include会把档案内容载入进来,而只是打通一个路径。或者说,import只是请编译器帮你打字,自动将名字填充完整。另外,因为java.lang用的非常频繁,几乎没有程序不适用它,因此它会被自动import。
- 继承
两者之间一定要通过IS-A测试,不然就不要使用继承。
如果两者之间只是有一段共同代码而已,可以考虑使用has-a。
有三种方法可以拒绝继承:
- 存取控制,就算类不能标记为私有,但它还是可以不标记公有。非公有的类只能被同一个包的类作出子类(继承);
- 使用final修饰符,表示该类是继承树的末端,不能被继承(也可以只在类内部对成员变量或者方法标识final,有final表示的成员无法被继承);
- 让类只拥有private的构造程序(这种方法我之前是不知道的,值得关注一下)。
- IS-A测试
Y是继承类X,类Z是继承类Y,那么Z应该能够通过IS-A X的测试。
- public, private, protect
- 既然有了抽象类这种方式,为什么还需要有抽象方法?
抽象方法是为了定义出一个全体子类都遵循的协议,因为抽象方法必须被重写!
- 接口
网上有许多答案,例如:多继承使得程序结构十分复杂,而且使用不多;单继承符合真实世界规律,一个儿子只有一个父亲。
接口是完全抽象的,因此接口中只有方法的定义,而不存在任何变量。接口只是定义一个规则,所有实现该接口的类都遵循这个规则。当然抽象类并不需要强制实现接口中的方法,但是具体类是一定要的。
简单的说,可以把接口就看成一个不含有变量的纯抽象类。
- Java中栈与堆空间
所有的对象都生活在堆空间。
方法和局部变量生活在栈空间。实例变量(成员变量)被声明在类而不是方法里面,实例变量存在于所属的对象中。局部变量包括方法的参数都是声明在方法中,它们是暂时的,且生命周期只限于方法被放在栈上的这段时间(从方法被调用到方法结束)。
当一个方法被调用时,该方法会放在栈顶(Head First Java的第237页图非常形象)。
实际上被堆上栈的是一个堆栈块,栈空间就这样划分为一个一个的方块。堆栈块中有方法以及方法的局部变量。
值得注意的是有关对象的局部变量。
例如:
void barf() {
Duck d = new Duck(24);
}
那么d会被放在栈上,记住d中存在的只是一个对Duck对象的引用。实际的这个被创建出来的Duck实例还是会放在堆上。
那么d会被放在栈上,记住d中存在的只是一个对Duck对象的引用。实际的这个被创建出来的Duck实例还是会放在堆上。
无论如何,对象本身都只存在于堆上。局部变量与方法存在于栈上。
- 构造函数
构造函数可以是公有、私有或不指定的。
如果没有super()调用父类的构造函数,编译器将自动在当前构造函数的第一句之前加入super()。
记住super()只会出现在当前构造函数的第一句。
例如:
public Boop() {
}
}
public Boop(int i) {
}
}
都是合法的。
public Boop(int i) {
size = i;
super();
}
}
是非法的。
从某个构造函数调用重载版的另一个构造函数。
我们可以使用this()来调用自身真正的构造函数,this()只能用在构造函数中,且它必须是第一行语句。
也就是说,this()与super()不能兼得。
class Mini extends Car {
Color color;
合法
public Mini() {
this(Color.Red);
}
}
public Mini(Color c) {
super("Mini");
super("Mini");
color = c;
}
非法
public Mini(int size) {
this(Color.Red);
super(size);
}
}
}
- 不让类实例化
- 取得新对象的方法
解序列化(deserialization)
Java Reflection API ?
- 静态变量的起始动作
静态项目的初始化有两项保证:
静态变量会在该类的任何对象创建之前就完成初始化。
静态变量会在该类的任何静态方法执行之前就初始化。
- 静态初始化程序(static initializer)
注意,类加载只有一次!不是创建类的实例,类的构造函数会在每个实例创建时执行。
静态的final变量必须初始化,其初始化的方法有两种,一种是在声明时初始化:
public class Foo {
public static final int X = 42;
}
}
另一种则是用静态初始化程序:
public class Foo {
public static final int X;
static {
X = 42;
}
}
}
}
否则,编译器会提示出错。
另外,这里补充一下,不只是静态的final变量,全部的final变量都需要初始化,可以是在声明时,也可以在构造函数里面。
- final
final的方法代表你不能覆盖掉该方法。
final的类代表你不能继承该类。
- 关于autoboxing,一个有趣的例子
Integer i;
int j;
public static void main(String[] args) {
TestBox t = new TestBox();
t.go();
}
}
public void go() {
j = i;
System.out.println(j);
System.out.println(i);
}
}
}
}
这个程序能够通过编译,却在执行时产生错误。
错误代码是go方法中的j=i语句,原因在于,i默认的初始化值为null,j为0。
- 将String转换成其他类型
int x = Integer.parseInt(s);
double d = Double.parseDouble("420.24");
boolean b = new Boolean("true").booleanValue();
注意,Boolean没有parseBoolean方法,不过Boolean的构造函数可以用String来创建对象。
- 数字的格式化
例子:
public class TestFormats() {
public static void main(String[] args) {
String s = String.format("i i j %,d k l m", 1000000000);
System.out.println(s);
}
}
}
}
输出:i i j 1,000,000,000 k l m
第一个参数是格式化串,之后的参数是要格式化的值。
格式化串可以带有实际上就是要这么输出而不用转译的字符。当遇到%字符时,它是会被方法其余参数替换掉的位置。
注意如果原本就要输出%,就需要用转义字符%:format("%1$+06.2f %%", 10.93);
格式化说明的格式:
在%后面最多有五个部分。下面的[]符号里面都是选择性的项目,因此只有%与type是必须的。格式化说明的顺序是固定的。
%[argument number$][flags][width][.percision]type
%:格式化说明开始。
[argument number$]:如果要格式化的参数超过一个以上,可以在这里指定是哪一个。
[flags]:特定类型的特定选项,例如数字是加逗号或正负号。
[width]:最小的字符数,注意:这不是总数,输出可以超过这个宽度,若不足则主动补零。书中是这么说的,但是我实际写程序发现是用空格符补足的。
[.percision]:精确度,注意:前面有个圆点符号。
type:一定要指明的类型标识。
补充,多个相同参数可以用<,而不用重复输入,例如:
System.out.println(String.format("%+06d %-6d", 123, 123));
和
System.out.println(String.format("%+06d %<-6d", 123));
效果相同,都是+00123 123
type
%d: decimal
format("%d", 42); => 42
参数必须能与int相容。
%f: floating point
format("%.3f", 42.0000000); => 42.000
参数必须是浮点数类型。
%x: hexadecimal
format("%x", 42); => 2a
参数必须是byte、short、int、long、BigInteger。
%c: character
format("%c", 42); => *
参数同上,但不包括BigInteger。character是将数字转译为其ASCII码对应的字符。
下面根据参数不同详细描述格式化说明,转载自
对整数进行格式化:
%[argument number$][flags][width]type
flags:
- 在最小宽度内左对齐,不可以与”用0填充“同时使用;
# 只适用于8进制和16进制,8进制时在结果前面加一个0,16进制时在前面加一个0x;
+ 正数前面加一个+;
' ' 正数前面加一个空格;
0 结果用0填充;
, 每三个数用,分隔;
( 负数去掉-号然后用括号括起来。
type:
d 十进制
o 八进制
x 十六进制
对浮点数进行格式化:
%[argument number$][flags][width][.percision]type
flags:除了没有#其余与整数相同。
type:
e,E 科学记数法表示的十进制
f 十进制
g,G 自动判断使用科学记数法还是普通方法
a,A 十六进制
对日期进行格式化:
举例:
String.format("%tc", new Date()); => Sun Nov 28 14:52:41 MST 2004
以下日期和时间转换的后缀字符是以'%t'和'%T'开始的,
时间格式化:
H 24小时制的小时,被格式化为必要时带前导零的两位数,即00-23;
I 12小时制的小时,被格式化为必要时带前导零的两位数,即01-12;
k 24小时制的小时,即0-23;
l 12小时制的小时,即1-12;
M 小时中的分,被格式化为必要时带前导零的两位数,即00-59;
S 分钟中的秒,被格式化为必要时带前导零的两位数,即00-60(“60”是支持闰秒所需的一个特殊值);
L 秒钟的毫秒,被格式化为必要时带前导零的三位数,即000-999;
N 毫秒中的微秒,被格式化为必要时带前导零的九位数,即000000000-999999999;
p 特定于语言环境的上午或下午,对于英语而言是"am"或"pm",使用'T'可以强制转换为大写。
Z 表示时区缩写的字符串
日期格式化:
B 特定于语言的月份全称,例如“February”和“January”;
b,h 特定于语言的月份简称,例如“Feb”和“Jan”;
A 特定于语言的星期几全称,例如“Sunday”和“Monday”;
a 特定于语言的星期几简称,例如“Sun”和“Mon”;
C 年份数除以100,带前导零的两位数;
Y 年份,被格式化为必要时带前导零的四位数;
y 年份的后两位数,带前导零的两位数;
j 一年中的天数,带前导零的三位数;
m 月份,带前导零的两位数,即01-13;
d 一个月中的天数,被格式化为带前导零两位数,即01-31;
e 一个月中的天数,不带前导零。
日期时间的组合:
R 24小时的时间,被格式化为"%tH:%tM";
T 24小时的时间,被格式化为"%tH:%tM:%tS";
r 12小时的时间,被格式化为"%tI:%tM:%tS %tp";
D 日期,被格式化为"%tm/%td/%ty";
F ISO 8601格式的完整日期,被格式化为"%tY-%tm-%td";
c 日期和时间,被格式化为"%ta %tb %td %tT %tZ %tY"。
补充,
对于Date对象,可以用new SimpleDateFormat("hh:mm:ss").format(date);
这种形式。
- Date和Calendar
Calendar用于操作日期,但是Calendar是一个抽象类,我们可以通过:
Calendar cal = Calendar.getInstance();
得到一个它的子类实例,这个子类是根据地区来的。
roll方法和add方法的区别在于,roll只是当前字段滚动不会影响到其他字段,举例:
cal.add(cal.DATE, 35);
当前日期加上35天,月份也会加1;
cal.roll(cal.DATE, 35);
当前日期加上35天,月份不会动。
roll就是一种不会进位的加法。
- RuntimeException
就是编译器不会去检查是否有RuntimeException没存在于try/catch块中。
try/catch是用来处理真正的异常,而不是程序的逻辑错误,该块要做的是恢复的尝试,或者至少会优雅的列出错误信息。
大部分的RuntimeException都是因为程序逻辑的问题,而不是以你所无法预测或防止的方法出现的执行期失败状况,你无法保证文件一直都在,你无法保证服务器不会死机等等。但是你可以确保程序不会运行不合理的逻辑,例如对只有5项元素的数组取第9个元素的值。
- try/catch/finally
例如:
try {
turnOvenOn();
x.bake();
} catch (BakingException ex) {
} catch (BakingException ex) {
ex.printStackTrace();
} finally {
} finally {
turnOvenOff();
}
}
与
try {
turnOvenOn();
x.bake();
turnOvenOff();
} catch (BakingException ex) {
} catch (BakingException ex) {
ex.printStackTrace();
turnOvenOff();
}
}
等价。
执行顺序:
如果try块失败了,抛出异常,流程会马上转移到catch块。当catch块完成时,会执行finally块。当finally块完成时,就会继续执行其余的部分。
如果try块成功了,流程会跳过catch块并移动到finally块,当finally块完成时,就会继续执行其余的部分。
如果try或catch块有return指令,finally还是会执行!流程会跳到finally块然后回到return指令。
- 异常的捕获
因此catch需要从小写到大,例如如果第一个catch是catch (Exception e),那么之后的catch都不会被用到。
- 异常处理规则
try与catch之间不能有程序;
try一定要有catch或finally;
只带有finally的try必须要声明异常。
- MIDI(Music Instrument Digital Interface)
对于真正要给音箱发出的声音而言,MIDI的数据还需送到某种MIDI装置上,并将数据转换成声音。在《Head First Java》中只讨论了使用软件装置来合成发声,以下是JavaSound的工作原理:
四项必备的条件:
发声的装置 Sequencer 把Sequencer想象成CD播放机
要演奏的乐曲 Sequence 把Sequence想象成单曲CD
带有乐曲信息的记录 Track 把Track想象成单曲CD上唯一歌曲的信息
乐曲的音符等信息 MidiEvent 可被CD播放机理解的信息数据,MIDI event就好像乐谱上的某一个音符记号,也可以用来表示更换乐器的指令等。
Sequencer plays Sequence, Sequence has a Track, Track holds MidiEvent s。
音符等指令组成一首歌,歌存在CD上,CD需要CD播放机才能转成实际发出的声音。
- MidiEvent
Message描述做什么,MidiEvent指定何时做。
创建Message
ShortMessage a = new ShortMessage();
置入指令
a.setMessage(144, 1, 44, 100);
用Message创建MidiEvent
MidiEvent noteOn = new MidiEvent(a, 1);
将MidiEvent加入到Track中
track.add(noteOn);
补充,Track带有全部的MidiEvent对象,Sequence会根据事件时间组织它们,然后Sequence会根据此顺序来播放,同一时间可以执行多个操作,例如和弦声音或不同乐器的声音。
补充,一个noteOn和一个noteOff两个事件才能组成一个信息。
- Message中信息的格式
信息类型:
144代表打开,128代表关闭;
频道:
每个频道代表不同的演奏者;
要发出的音符:
从0-127代表不同的音高;
音道:
用多大的音道按下?0几乎听不到,100算是差不多。我想应该是音量?
- 图形界面按钮的一致性问题
要取得跨平台的相同外观就要使用Metal,不然就不要指定,让外观使用平台的默认值。
- 内部类
内部类把存取外部类的方法和变量当做是开自家的冰箱。
内部类如何实例化?如何明确调用?怎么确定哪个是自己的实例冰箱?
内部类的实例一定会绑在外部类的实例上。
内部类的实例化:
1.在外部类中像实例化正常的类一样进行实例化:
public class TestInner {
public class TestInner {
String q = "**";
InnerClass ic = new InnerClass();
class InnerClass {
int x;
public void pq() {
System.out.println(q);
px();
}
}
2.在外部类以外的程序代码来初始内部实例:
Class Foo {
Class Foo {
public static void main(String[] args) {
MyOuter outerObj = new MyOuter();
MyOuter.MyInner innerObj = outerObj.new MyInner();
}
}
}
}
内部类提供了一个在类中重复实现同一个方法的方法。
也提供了重复实现同一个接口的方法。
尤其是在GUI事件处理中,如果你想要让3个按钮有不同的事件行为,就要使用3个内部类来分别实现ActionListener,也就是每个类实现自己不同的actionPerformed()方法。因为,如果用外部类实现的话,它们无法获取除了事件以外的很多界面信息。
- Swing组件
除了JFrame之外,交互组件与背景组件的差异不太明确。举例来说JPanel通常用在背景上,但是也可以与用户交互。就跟其他组件一样,你也可以向JPanel注册鼠标的点选等事件。
- 创建GUI简单四步
JFrame frame = new JFrame();
创建组件:
JButton button = new JButton("click me");
把组件加到frame上:
frame.getContentPane().add(BorderLayout.EAST, button);
显示出来:
frame.setSize(300, 300);
frame.setVisible(true);
- 布局管理器
面板布局管理器会询问每个组件理想的大小应该是什么。
面板布局管理器以它的布局策略来决定是否应该要尊重全部或部分按钮的理想。
三大布局管理器
BorderLayout
这个管理器把背景组件分成5个区域,每个被管理的区域只能放上一个组件。由此管理员安置的组件通常不会取得默认的大小。这是框架默认的布局管理器。
FlowLayout
这个管理器的行为跟文本处理程序的版面配置方式差不多。每个组件会依照理想的大小呈现,并且会从左到右依照加入的顺序以可能会换行的方式排列。因此在组件放不下的时候会被放到下一行。这是面板默认的布局管理器。
BoxLayout
它就像FlowLayout一样让每个组件使用默认的大小,并且按照加入的顺序来排列。但BoxLayout是以垂直的方向来排列(也可以水平,但通常我们只在乎垂直方式)。不像FlowLayout会自动换行,它让你插入某种类似换行的机制来强制组件从新的一行开始排列。
- BorderLayout
南北先占位,南北宽度不能被满足,由背景组件宽度决定;
东西再占位,东西高度由背景组件高度减去南北高度限制;
中间最后捡剩下的部分。
- FlowLayout
- BoxLayout
a.setLayout(new BoxLayout(a, BoxLayout.Y_AXIS));
- JFrame为什么特殊
JFrame会特殊是因为它是让事物显示在画面上的接点。因为Swing的组件纯粹由Java组成,JFrame必须要连接到底层的操作系统来存取显示装置。我们可以把面板想象成安置在JFrame上的100%纯Java层。或者把JFrame想做是支撑面板的框架。你甚至可以用自定义的JPanel来换掉框架的面板:
myFrame.setContentPane(myPanel);
简而言之,JFrame就是在底层操作系统的显示装置与Java的Swing组件之间的一座桥。
- 保存对象
如果只有自己写的Java程序会用到这些数据:
序列化(serialization);
如果数据需要被其他程序引用:
纯文本文件保存。
- 序列化
当对象被序列化时,被该对象引用的实例变量也会被序列化。且所有被引用的对象也会被序列化。序列化程序会将对象版图上的所有东西存储起来。被对象的实例变量所引用的所有对象都会被序列化。
也正是如此,所以一个可以被序列化的类,它的子类,它成员变量所引用的对象所属的类,都应该能够被序列化。
当一个类的成员变量所引用的对象所属的类没有实现序列化接口时,不会出现编译错误,如果该成员变量没有被实例化的话,也不会出现任何错误,但是一旦实例化,就会出现NotSerializableException。
那么如果别人的类忘记实现序列化接口了怎么办,此时,我们可以将实例变量标记为transient(瞬时的)。
如果你需要序列化程序能够跳过某个实例变量,就把它标记为transient,例如:
import java.net.*;
class Chat implements Serializable {
transient String currentID;
String username;
... ...
}
}
currentID就不会被存储,username会被序列化存储。
当恢复的时候,transient标记的变量会以null返回。主数据类型会以默认值返回。
如果两个对象都有引用实例变量指向相同的对象会怎么样?例如两个Cat都有相同的Owner对象,那Owner会被保存两次吗?
序列化会分辨两个对象是否相同,在此情况下只有一个对象会被存储,其他引用会复原成指向该对象。
如果对象在继承树上有个不可序列化的祖先类,则该不可序列化类以及在它之上的类的构造函数(就算是可序列化也一样)就会执行。一旦构造函数连锁启动之后将无法停止。也就是说,从第一个不可序列化的父类开始,全部都会重新初始状态。
静态变量不会被序列化。当对象还原时,静态变量会维持类中原本的样子,而不是存储时的样子。
- File类
关于File有个很有用的功能就是它提供一种比使用字符串文件名来表示文件更加安全的方式。举例来说,在构造函数中取用字符串文件名的类也可以用File对象来代替该参数,以便检查路径是否合法等,然后再把对象传给FileWriter或者FileOutputStream。
可以对File对象做的事情:
创建磁盘文件:
File f = new File("MyCode.txt");
建立新目录:
File dir = new File("Chapter7");
dir.mkdir();
列出目录下的内容:
if (dir.isDirectory()) {
String[] dirContents = dir.list();
for (String s : dirContents) {
System.out.println(s);
}
}
}
}
取得路径的绝对地址:
dir.getAbsolutePath();
删除目录或文件:
f.delete();
等等。
- 序列化的识别
删除实例变量;
改变实例变量的类型;
将非瞬时的实例变量改为瞬时的;
改变类的继承层次;
将类从可序列化改成不可序列化;
将实例变量改成静态的。
通常不会有事的修改:
加入新的实例变量(还原时会使用默认值);
在继承层次中加入新的类;
从继承层次中删除类;
不会影响解序列化程序设定变量值的存取层次修改;
将实例变量从瞬时改成非瞬时的(使用默认值还原)。
- 网络Socket连接
要创建Socket连接你得知道两项关于服务器的信息:它在哪里以及用哪个端口来收发数据。也就是IP地址与端口号。
TCP端口只是一个16位宽,用来识别服务器上特定程序的数字。
网页服务器(HTTP)的端口号是80,这是规定的标准。
Telnet服务器的端口号是23。
POP3邮件服务器的端口号是110。
SMTP邮局交换服务器的端口号是25。
FTP的端口号是20。
Time的端口号是37。
HTTPS的端口号是443。
端口号用于识别要连接到哪个应用程序。
每个服务器上都有65536个端口(0~65535),它只是个逻辑上用来表示应用程序的数字。
从0~1023的TCP端口号是保留给已知的特定服务使用。所以我们可用的范围是1024~65535。
- 简单的服务器程序
工作方式:
服务器应用程序对特定端口创建出ServerSocket。
ServerSocket serverSocket = new ServerSocket(4242);
这会让服务器应用程序开始监听来自4242端口的客户端请求。
客户端对服务器应用程序建立Socket连接。
Socket sock = new Socket("190.165.1.103", 4242);
客户端必须知道地址与端口号。
服务器创建出与客户端通信的新Socket。
Socket sock = serverSocket.accept();
accpet()方法会在等待用户的Socket连接时闲置着。一旦用户连接上来,此方法会返回一个Socket(在监听端口(此例中是4242)以外的端口上)以便与客户端通信。Socket与ServerSocket不同,因此ServerSocket可以空出来等待其他用户。
我的理解就是,ServerSocket是服务器上一个监听的端口,当它收到一个来自用户的请求时,便会在另外一个端口建立一个与用户的连接。
- 多线程
简单的,如何启动新的线程:
建立Runnable对象(线程的任务)
Runnable threadJob = new MyRunnable();
此类就是你对线程要执行的任务的定义,也就是说此方法会在线程的执行空间运行。
建立Thread对象(执行工人)并赋值Runnable(任务)
Thread myThread = new Thread(threadJob);
把Runnable对象传给Thread的构造函数。这会告诉Thread对象要把哪个方法放在执行空间去运行——Runnable的run()方法。
启动Thread
myThread.start();
在没有调用Thread的start()方法之前,我们只是建立了一个Thread类的实例。只有当它启动以后,它才会把Runnable对象的方法摆到新的执行空间中。
对Thread而言,它是个工人,而Runnable就是这个工人的工作。Runnable带有会放在执行空间的第一项方法:run()。
Runnable这个接口只有一个方法:
public void start()。
当把Runnable传给Thread的构造函数时,实际上就是在给Thread取得run()的办法。这就等于你给了Thread一项任务。
- 新建Thread的三个状态
Thread t = new Thread(r);
可执行
t.start();
执行中
当调用start方法之后,线程就会进入可执行状态。但是何时执行得由Java虚拟机决定。Java虚拟机的线程调度机制决定了线程何时进入执行状态,何时由执行状态转入可执行状态。程序员有时能对Java虚拟机选择执行线程给出意见,但无法强迫它把线程从可执行状态转到执行中。
一旦线程进入可执行状态,它会在可执行与执行中两种状态来来去去,同时也有另外一种状态:暂时不可执行(又被称为堵塞)。
线程有可能会暂时被挡住:
调度器(scheduler)会因为某些原因把线程送进去关一阵子。例如线程可能执行到等待Socket输入串流的程序段,但没有数据可供读取。调度器会把线程移出可执行状态,或者线程本身的程序要求小睡一下(sleep())。也有可能是因为线程调用某个被锁住(locked)的对象上的方法。此时线程就得等到锁住该对象的线程放开这个对象才能继续下去。
这类型的条件都会导致线程暂时失能。
- 线程创建的补充
这是另外一种创建线程的方法。从面向对象的观念来看,Thread与线程的任务是非常不同的活动,也是不同的类。你唯一会想要子类/继承Thread的目的是建立出更特殊的Thread。也就是说如果把Thread当作工人来看的话,除非有很特殊的工作行为,不然你不会继承Thread。如果你只是要有个工人来运行一般的任务,建立实现Runnable的独立类给工人运行就好。
这跟设计概念有关,而不影响性能或语言用法的好坏。将Thread做个子类来覆盖掉run()是完全合法的,但通常不是个好主意。
- Thread对象能否重复使用,能否调用start()指定新的任务给它
一旦线程的run()方法完成之后,该线程就不能再重新启动。事实上过了该点线程就会死翘翘。Thread对象可能还呆在堆上,如同活着的对象一般还能接受某些方法的调用,但已经永远地失去了线程的执行性,只剩下对象本身。
- SLEEP
- 并发性(Concurrency)问题
这一切都来源于可能发生的一种情况:两个或以上的线程存取单一对象的数据。
synchronized关键词
使用synchronized这个关键词来修饰的方法使它每次只能被单一的线程存取。
synchronized关键词代表线程需要一把钥匙来存取被同步化(synchronized)过的线程。
要保护数据,就把作用在数据上的方法给同步化。
注意,锁不是在方法上的,虽然我们将synchronized关键词标记在方法上,但是锁不是在方法上的,锁是在对象上的。
每个对象只有一把锁,每个锁只有一把钥匙。
如果对象有两个同步化方法,就表示两个线程无法进入同一个方法,也表示两个线程无法进入不同的方法。线程只有在取得对象锁的钥匙时才能进入同步化的方法。
如果有多个方法会操作对象的实例变量,则这些方法都应该有同步化的保护。
同步化的目标是保护重要的数据。但要记住,我们锁住的不是数据而是存取数据的方法。
同步化也可以标记在静态化的方法,用于保护静态变量。这个锁在类上。除了对象以外,类本身也有一个锁。举例来说,如果有3个Dog对象在堆上,则总共有4个与Dog有关的锁。3个是Dog实例的,1个是类的。
当线程遇上同步化的方法时,线程会认识到它需要对象的钥匙才能进入该方法。它会取得钥匙(这是由Java虚拟机来处理,没有存取对象锁的API可用),如果可以拿到钥匙才会进入方法。
也可以只同步化一行或数行指令而不必整个方法同步化:
public void go() {
doStuff();
synchronized(this) {
criticalStuff();
moreCriticalStuff();
}
}
}
- 泛型
泛型的类代表类的声明要用到类型参数,泛型的方法代表方法的声明特征用到类型参数。
使用定义在类声明的类型参数:
public class ArrayList<E> extends AbstractList<E> implements List<E> ... {
public boolean add(E o);
public boolean add(E o);
...
}
参数的类型声明基本上会以用来初始化类的类型来取代。
使用未定义在类声明的类型参数:
使用未定义在类声明的类型参数:
public <T extends Animal> void takeThing(ArrayList<T> list) {
...
...
}
如果类本身没有使用类型参数,你还是可以通过在一个不寻常但可行的位置上指定给方法——在返回类型之前。
如果类本身没有使用类型参数,你还是可以通过在一个不寻常但可行的位置上指定给方法——在返回类型之前。
补充,
一定要区别:
public <T extends Animal> void takeThing(ArrayList<T> list)
与
public void takeThing(ArrayList<Animal> list)
<T extends Animal>是方法声明的一部分,表示任何被声明为Animal或Animal的子类(像是Cat或Dog)的ArrayList是合法的。因此可以使用ArrayList<Animal>、ArrayList<Cat>或者ArrayList<Dog>等来调用前者。
但是对于后者而言,只有ArrayList<Animal>是合法的。
值得注意的是,在泛型中extends包括了extends和implements,也就是说,上例中Animal是一个类,所以T既可以是Animal的扩展(Animal本身或者Animal的子类),如果Animal是一个接口,那么T就是一个实现了Animal接口的扩展。
- 引用相等性和对象相等性
堆上同一对象的两个引用。
引用到堆上同一个对象的两个引用是相等的。如果对两个引用调用hashCode(),会得到相同的结果。如果没有被覆盖,hashCode()默认的行为会返回每个对象特有的序号(大部分的Java版本是依据内存位置计算此序号,所以不会有相同的hashcode)。
可以通过“==”运算符,比较两个引用的字节组合。如果引用的对象相同,则字节组合也一样。
对象相等性:
通过覆盖equals方法自己定义。
- HashSet如何检查重复
但是hashcode相同,两个对象也不一定相等(?)。
所以如果HashSet找到两个hashcode相同的对象,它会调用其中一个的equals来检查hashcode相等的对象是否真的相同。
?:因为我们知道hash算法也有可能发生碰撞,所以就算对两个不同的对象求取hash值,它们也还是可能返回一个相同的hash值。
- hashCode()与equals()的相关规定
如果两个对象相等,对其中一个调用equals()必须返回true;
如果两个对象有相同的hashcode值,它们也不一定是相等的;
因此若equals()被覆盖过,则hashCode()也必须被覆盖。
- 数组的类型检查与集合的类型检查
这里,数组指的是“[]”,集合指的是Collections以及Map本身和它们的子类。
- 万用字符
public void takeAnimals(ArrayList<? extends Animal> animals) {
for (Animal a : animals) {
a.eat();
}
}
}
}
注意,此处的extends包括了继承与接口。
在使用带有<?>的声明时,编译器不会让你加入任何东西到集合中。
- Java部署的选择
Executable Jar
整个程序都在用户的计算机上以独立、可携的GUI执行,并以可执行的Jar部署。
两者之间:
Web Start, RMI app
应用程序被分散成在用户本地系统运行的客户端,连接到执行应用程序服务的服务器端。
完全在远程:
HTTP
整个应用程序都在服务器端执行,客户端通过非Java形式、可能是浏览器的装置来存取。
- Java Web Start
当Java Web Start下载你的程序(可执行的JAR)时,它会调用程序的main()。然后用户就可以通过JWS helper app启动应用程序而不需回到当初的网页。
JWS能够检测服务器上应用程序局部(例如某个类文件)的更新——在不需要用户介入的情况下,下载与整合更新过的程序。
JWS工作方式:
客户端点击某个网页上JWS应用程序的链接;
Web服务器收到请求发出.jnlp文件(不是JAR)给客户端的浏览器;
.jnlp文件是描述应用程序可执行JAR文件的XML文件。
客户浏览器启动Java Web Start,JWS的helper app读取.jnlp文件,然后向服务器请求MyApp.jar;
Web服务器发送.jar文件;
JWS取得.jar文件并调用指定的main()来启动应用程序。
然后用户就可以在离线的情况下通过JWS来启动应用程序。
部署步骤:
将程序制作成可执行的JAR;
编写.jnlp文件;
把.jnlp与JAR文件放到Web服务器;
对Web服务器设定新的mime类型application/x-java-jnlp-file;
这会让服务器以正确的header送出.jnlp数据,如此才能让浏览器知道所接收的是什么。
设定网页连接到.jnlp文件。
- .jnlp
<?xml version = "1.0" encoding = "utf-8"?>
<jnlp spec = "0.2 1.0"
codebase = "http://127.0.0.1/~kathy"
//codebase用来指定相关文件的起始目录
href = "MyApp.jnlp"
//相对于codebase的位置路径,它也可以放在某个目录下
<information>
<title>kathy app</title>
<vendor>Wickedly Smart</vendor>
<homepage href = "index.html"/>
<description>Head First WebStart demo</description>
<icon href = "kathys.gif"/>
<offline-allowed/>
//设置成离线也可执行
</information>
//这些tag是必须加入的,information是给helper app使用的,可供显示程序的信息
<Resource>
<j2ee version = "1.3+"/>
//指定需要1.3或之后版本的Java
<jar href = "MyApp.jar"/>
//可执行的JAR名称
</Resource>
<application-desc main-class = "HelloWebStart"/>
//就和manifest一样描述哪个带有main()函数
</jnlp>
- JWS与applet有什么区别
浏览器会使用Java的plug-in来执行applet。
applet没有类似程度的自动更新功能,且一定得从浏览器上面执行。
JWS一旦下载后,就可以脱离浏览器离线执行程序了。
- RMI(Remote Method Invocation)
这就是RMI能够带给你的功能。
调用方法的过程:
客户端对象对客户端辅助设施对象调用doBigThing();
客户端对象以为它是与真正的服务沟通,它以为辅助设施就是服务。
客服端辅助设施对象把调用信息打包通过网络送到服务端辅助设施对象;
客户端辅助设施假装成服务,但只是真品的代理人而已。
服务端辅助设施对象解开来自客户端辅助设施对象的信息,并以此调用真正的服务对象的doBigThing()方法;
服务端辅助设施收到请求,解开包装,调用真正的服务。
Java RMI提供客户端和服务器端的辅助设施对象!
中间的信息是如何从Java虚拟机送到Java虚拟机要看辅助设施对象所用的协议而定。
使用RMI时,必须要决定协议:JRMP或者IIOP。
JRMP是RMI的原生协议,它是为了Java对Java间的远程调用而设计的。
IIOP是为了CORBA(Common Object Request Broker Architecture)而设计的,它让你能够调用Java对象或其他类型的远程方法。
在RMI中,客户端辅助设施称为stub,而服务端称为skeleton。
服务器端创建远程服务:
- 创建Remote接口
远程的接口定义了客户端可以调用的方法,它是个作为服务的多态化类。stub和服务都会实现此接口。
继承java.rmi.Remote
Remote是个标志性接口,意味着没有方法。然而它对RMI有特殊的意义,所以必须遵守这条规则。注意这里使用的是extend,接口可以继承其他的接口。
public interface MyRemote extends Remote {
声明所有的方法都会抛出RemoteException
远程的接口定义了用户可以远程调用的方法,它是个作为服务的多态化类。也就是说,客户端会调用有实现此接口的stub,而stub因为会执行网络的输入/输出工作,所以可能会发生各种问题。客户端必须处理或声明异常来认知这一风险。如果方法在接口中声明异常,调用该方法的所有程序都必须处理或再声明此异常。
public interface MyRemote extends Remote {
public String sayHello() throws RemoteException;
}
}
确定参数和返回值都是primitive主数据类型或Serializable
任何远程方法的参数都会被打包通过网络传输,而这是通过序列化来完成的。返回值也一样。
- 实现Remote
这是真正执行的类,它实现出定义在接口上的方法。它是客户端会调用的对象。
实现Remote接口
你的服务必须要实现Remote接口——就是客户端会调用的方法。
public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote {
public String sayHello() {
return "Servers says, 'Hey'";
}
}
}
继承UnicastRemoteObject
为了要成为远程服务对象,你的对象必须要有与远程有关的功能。其中最简单的方式就是继承UnicastRemoteObject以让这个父类处理这些工作。
编写声明RemoteException的构造函数
之所以要这么做,只是因为UnicastRemoteObject的构造函数会抛出RemoteException。当类被初始化时,父类的构造函数一定会被调用,如果父类构造函数抛出异常,你也得声明你的构造函数会抛出异常。
向RMI registry注册服务
有了服务之后,需要将它注册,以便远程客户存取。这可以通过将它初始化并加入RMI registry。当你注册对象时,RMI系统会把stub加到registry中,因为这是客户端所需要的。使用java.rmi.Naming的rebind()来注册。
try {
MyRemote service = new MyRemoteImpl();
Naming.rebind("Remote Hello", service);
//帮服务命名(客户端会靠名字查询registry),并向RMI registry注册,RMI会将stub做交换并把stub加入registry
} catch (Exception ex) {...}
} catch (Exception ex) {...}
- 用rmic产生stub与skeleton
客户端和服务器都会有helper。你无须创建这些类或是产生这些类的源代码。这都会在你执行JDK所附的rmic工具时自动处理掉。
对实现的类(不是Remote接口)执行rmic
伴随Java Software Development Kit而来的rmic工具会以服务的实现产生两个新的类:stub和skeleton。它会按照命名规则在你的远程实现名称后面加上_Stub和_Skeleton。
命令行输入:
rmic MyRemoteImpl
产生:
MyRemoteImpl_Stub和MyRemoteImpl_Skeleton
要在当前目录执行。
- 启动RMI registry(rmiregistry)
rmiregistry就像是电话薄。用户会从此处取得代理(客户端的stub/helper对象)。
调出命令行来启动rmiregistry
要确定你是从可以存取到该类的目录来启动。
命令行输入:
rmiregistry
- 启动远程服务
你必须让服务对象开始执行。实现服务的类会开始服务的实例并向RMI registry注册。要有注册后才能对用户提供服务。
启动服务
java MyRemoteImpl
客户端取得stub对象:
客户端通过RMI registry取得stub对象。
就像查询电话薄一样,找出名字相符的服务。
MyRemote service = (MyRemote) Naming.lookup("rmi://127.0.0.1/Remote Hello");
客户端必须使用与服务相同的类型,事实上,客户端不需要知道具体实现服务的类名称,只用接口名称就行。
查询返回结果是Object,需要类型转换。
lookup()是个静态方法。
主机名称或者ip地址。
名称必须跟注册的名称一样。
客户端查询RMIregistry
Naming.lookup("rmi://127.0.0.1/Remote Hello");
RMI registry返回stub对象
RMI会自动将stub解序列化。
客户端就像取用真正的服务一样的调用stub上的方法。
客户端必须有stub类(例子中是MyRemoteImpl_Stub.class),否则stub在客户端就无法被反序列化。客户端也需要调用远程对象方法所返回的序列化对象的类(例子中是MyRemote)。如果是一个简单的系统,可以简单地把这些类移交到客户端。
另外还有一个方法是“动态类下载”(dynamic class downloading),利用动态下载,序列化的对象可以被贴上一个URL,告诉客户的RMI系统去寻找对象的类文件。在反序列化的过程中,如果RMI没有在本地发现类,就会利用HTTP的GET从该URL取得类文件。所以你需要一个简单的Web服务器来提供这些文件。
- Servlet和EJB
servlet是EJB的用户。此时,servlet是通过RMI与EJB通信的。
大致过程:
用户填写网页上的表格并提交。HTTP服务器收到请求,判断出是要给servlet的,就将请求传送过去。
servlet开始执行,把数据保存到数据库中,然后组合出返回给浏览器的网页。
创建并执行servlet的步骤:
找出可以存放servlet的地方
假设已经有网页服务器可以运行servlet。最重要的事情是找到哪里可以存放servlet文件让服务器可以存取。
取得servlets.jar并添加到classpath上
servlet并不是Java标准库的一部分,需要下载servlets.jar包。
通过extend过HttpServlet来编写servlet类
public class MyServletA extends HttpServlet
编写HTML来调用servlet
当用户点击引用到servlet的网页链接时,服务器会找到servlet并根据HTTP的GET、POST等命令调用适当的方法。
<a href = "servlets/MyServletA">This is the most amazing servlet.</a>
给服务器设定HTML网页和servlet
- JSP与Servlet
这样就能让你在编写一般HTML网页的时候还能同时掌握动态内容的能力,内嵌的Java代码(还有其他能够触发Java程序代码的标识)可以于执行时处理。
JSP主要好处在于能更容易地编写HTM部分而不会像servlet一样出现一堆难以阅读的print命令。
- Jini
adaptive discovery(自适应搜索)
self-healing networks(自恢复网络)
使用Jini时,用户只要知道服务所实现的接口就行。
Jini的查询服务比RMI的registry更强更有适应性,因为Jini会在网络上自动的广告。当查询服务上线时,它会使用IP组播技术送出信息给整个网络说:“大爷我就在这里,想找东西就问我”。不只是这样,如果客户端在查询服务已经广播之后上线,客户端也可以发出信息给网络说:“那个谁在不在”。
当服务上线时,它会自动地探索网络上的Jini查询服务并申请注册。注册时,服务会送出一个序列化的对象给查询服务。一旦取得查询服务的引用,客户就可以查询“有没有东西实现ScientificCalculator”。此时若查询服务找到,就会返回服务所放上来的“ScientificCalculator”序列化对象。
- 位运算符
以指定量右移字节,左方补0,符号不变。
无符号右移运算符:>>>
无符号右移运算符:>>>
与>>一样,但符号位也会参与,第一位也会补0,正负号可能会改变。
左移运算符:<<
与>>>一样,但方向相反,右方补0,正负号可能会改变。
- String与包装类的不变性
包装类的两个主要用途:
将primitive主数据类型包装成对象;
使用静态的工具方法。
包装对象创建后就无法改变该对象的值,只能通过新建立包装对象。包装对象没有setter。
- 存取权限和存取修饰符
public
代表任何程序代码都可以存取的公开事物(类、变量、方法、构造函数等)
protected
受保护的部分运行起来像default,但也能允许不在相同包的子类继承受保护的部分
default
只有在同一包中的默认事物能够存取
private
只有同一类中的程序代码才能存取
存取修饰符
public
用public来设定打算要开发给其他程序代码的类、常数(static final变量)、方法,以及大部分的构造函数
default
protected与default权限等级都是作用在包上。
只有同一包也是默认等级的程序可以存取。
在默认类中的public方法意味着此方法并不全然是public的,只有同一个包中的其他类可以使用它。
protected
它与default几乎一样,只有一处不同:允许不同包中的子类继承它的成员。这就是protected带来的功能——不在同一个包的子类。
如果不同包的子类有对父类实例的引用,子类无法透过此引用存取父类的protected方法!唯一的办法是通过继承取得此方法。
private
设定给大部分的实例变量,以及不想公开的方法。
- 关于RMI补充
try {
//创建一个远程对象,这个类是extends UnicastObjectRemote implements Remote的
Hello rhello = new HelloImpl();
//创建本地主机上的远程对象注册表Registry实例,并指定端口(Java默认的是1099),必不可少的一步,缺少注册表创建,则无法绑定对象到远程注册表上
LocateRegistry.createRegistry(8888);
//把远程对象注册到服务器上,并命名为RHello
//绑定URL标准格式为:rmi://host:post/name(其中协议名可以省略,下面两种写法都ok)
Naming.bind("rmi://localhost:8888/RHello", rhello); //或者 Naming.bind("//localhost:8888/RHello", rhello);
} catch (Exception ex) {
ex.printStackTrace();
}
客户端:
try {
Hello rhello = Naming.lookup("rmi://localhost:8888/RHello");
System.out.println(rhello.helloWorld());
} catch (Exception ex) {
ex.printStackTrace();
}