Java学习笔记

把Java学习过程中的一些知识点记下来,加深印象以及方便日后复习

主要参考资料:Thinking in Java, 4th Edition, Bruce Eckel


1 基本概念

1.1 Java的数据类型

  • Java有两种数据类型:基本数据类型和引用数据类型
  • 基本数据类型:3类8种,包括数值型(整数型:byte、short、int、long,浮点型:float、double),字符型char,布尔型boolean
  • 引用数据类型:类class,接口interface,数组
数据类型 占用存储空间 表数范围 备注
byte 1字节 -2^7 — 2^7-1 1 byte = 8 bit
short 2字节 -2^15 — 2^15-1
int 4字节 -2^31 — 2^31-1 整型默认为int
long 8字节 -2^63 — 2^63-1 赋值long型数字后面要加L
float 4字节 -3.403E38 — 3.403E38 赋值float型数字后面要加F
double 8字节 -1.798E308 — 1.798E308 浮点型默认为double
char 2字节
boolean 1 bit 不能用0 1代替true false
引用数据类型 4字节 代表的是对象的地址
  • 浮点型不是精确的,有误差,一般不用于两数比较;如果要精确计算,需要使用java.math包下面的类BigInteger和BigDecimal
  • char类型赋值用单引号,用来表示Unicode编码表中的字符(中文也可以)(String是字符序列,用双引号)char也可表示转义符

1.2 主类型的默认值

boolean char byte short int long float double
false null 0 0 0 0L 0.0f 0.0d

1.3 不同进制数的表达

  • 八进制整数:以0开头,例如015
  • 十六进制整数:以0x或0X开头,例如0x15
  • 二进制整数:以0b或0B开头,例如0b101

1.4 变量与常量

  • 局部变量:方法或语句块内部定义的变量,必须先初始化再使用
  • 成员变量:方法外部、类内部定义的变量,会自动获得默认初始值
  • 静态变量:使用static定义,从属于类
  • 常量:用final修饰

1.5 类 class

  • 一个类实际是指“一类对象”,例如鸟类、鱼类,从属于这个类的所有对象都共享这些特征与行为,所以“类”是对属于这一类的所有对象的外观及行为进行的一种描述
  • 类名+方法名:即此方法返回值返回的是指向这个类的一个对象(的句柄)

1.6 方法 method

  • 方法的声明格式:[修饰符1 修饰符2] 返回值类型 方法名(形参列表){语句}
  • 形参:用于接收外界传入的数据;实参:调用方法时实际传递给方法的数据;返回值类型:返回的数据类型,如无则必须指定为void
  • 传递实参时传递的是数据的副本
  • return语句:终止方法的运行并返回值,可以直接return结束方法

1.7 命名规范

  • 首位可以是字母、下划线、美元符
  • 类名:每个单词的首字母大写
  • 方法名和变量名:第一个单词小写,从第二个单词开始首字母大写,即“驼峰原则”
  • 常量名:全部大写,下划线连接

2 运算符

2.1 算术运算符

  • 二元运算符:+, -, *, /, %
  • 如果两个操作数有一个为long,则结果也为long,否则为int;如果两个操作数有一个为double,则结果也为double,否则为float;取余的余数符号与左边操作数相同
  • 一元运算符:++, --
  • 自增自减;b=a++,a先赋值给b,再自增;b=++a,a先自增,再赋值给b

2.2 扩展运算符

  • +=, -=, *=, /=
  • 说明:a+=b,即a=a+b

2.3 赋值运算符

  • =

2.4 关系运算符

  • >, <, >=, <=, ==, !=, instanceof
  • 关系运算的结果是布尔值;==和!=适用于所有数据类型;>,<,>=,<=仅适用于数值型和字符型;instanceof用于判断左边的对象是否由右边的类或子类所创建

2.5 逻辑运算符

  • &(与),|(或),!(非),&&(短路与),||(短路或),^(异或)
  • 短路与:若第一个是false,则第二个不计算,结果直接为false;短路或:若第一个是true,则第二个不计算,结果直接为true
  • 优先级:非>与>或

2.6 位运算符

  • &(按位与),|(按位或),^(按位异或),~(取反),>>(右移),<<(左移)
  • 左移1位相当于乘2,右移1位相当于除2取商

2.7 条件运算符

  • ? :
  • 此为三元运算符,例:a?b:c,即如果a是true,则结果为b,否则结果为c

3 初始化机制

3.1 构造器(构造方法)constructor

  • 构造器是一种特殊的方法,方法的所有特性都适用于它,但没有返回值
  • 构造器的名字与类名相同,可以有自变量
  • 如果没有定义构造器,编译器会自动定义一个无参构造器(或叫默认构造器),如果已定义则不会添加
  • 一个类可以有多个构造器,即方法过载

3.2 过载(重载)overload

  • 过载的意义:为了让相同的方法名伴随不同的自变量使用
  • 方法名相同,但形参个数、类型、顺序不同,即构成过载,是不同的方法,即每个过载的方法都必须采取独一无二的自变量类型列表
  • 如果只有返回值不同,或者只有参数名不同,不构成过载

3.3 this

  • 使用范围:方法内部
  • 使用方法:
1
2


3.4 static

  • 在static方法中不可直接访问非static成员

3.5 枚举类型

  • enum关键字使得在需要群组并使用枚举类型集时,可以很方便,使用enum时需要创建一个该类型的引用,并将其赋值给某个实例
  • enum实际上是一个类,并且它可以在switch-case语句里面使用
1
2
3
4
5
6
7
8
9
10
11
12
//Spiciness.java
public enum Spiciness {
NOT, MILD, MEDIUM, HOT, FLAMING
}

//SimpleEnumUse.java
class SimpleEnumUse {
public static void main(String[] args) {
Spiciness howHot = Spiciness.MEDIUM;
System.out.println(howHot);
}
}

4 访问权限控制

4.1 包 package

  • 包用来管理类,解决类重名的问题
  • package的声明语句必须作为文件的第一个非注释语句出现
  • package名称一般用公司或者作者的域名倒写,再加上模块名,保证独一无二

4.2 访问控制符

  • 使用访问控制符的原因:一是规定用户哪些能用、哪些不能用,防止用户接触他们不应接触的东西;二是将接口与实施细节分离,隐藏实施细节,这样做修改调整时不会对用户产生影响
  • 鉴于以上两点原因,在阅读别人的源码时,可以先着重查看public成员,因为它们可从外部访问,是对用户来说最重要的部分,而非公共成员往往是实施细节的一部分了
访问控制符 同一个类 同一个包中的类 其他包中的子类 所有类 备注
private 只有自己类能用
default 没有修饰符即是default,只有同一个包中的类能访问
protected 可以被同一个包的类以及其他包中的子类访问
public 可以被所有包中所有类访问
  • private:只有自己类能用;将一个方法设为private,可以防止在其他地方更改或删除;如果将类的默认构造器设为private,可以防止对这个类的继承
  • 每个Java源文件只能有一个public类,并且类名与文件名必须相同;允许Java源文件没有任何public类
  • 不可将类设为private或protected,类只能是public或default;如果不愿其他人访问这个类,可将其构造器设为private
  • 没有明确包名并且位于相同目录中的不同文件,Java把它们视为那个目录“默认包”的一部分,即这一目录下的其他文件都能对其访问

5 类再生

5.1 合成 composition

  • 合成是指通过new关键字在新类中创建原有类的对象,以实现类再生和代码的重复使用

5.2 继承 inheritance

  • 使用extends关键字实现类的继承
  • 继承的一个好处:支持累积开发,即允许引入新的代码,同时不会为现有代码造成错误,将新错误隔离在新代码里面
  • Java的类只有单继承,即只有一个父类,不像C++有多继承(但Java的接口可以多继承)
  • 子类继承父类后,可以得到父类的所有属性和方法(除了父类的构造方法),但不一定可以直接访问,比如父类中private的属性和方法
  • 如果定义一个类时没有用extends,那么默认父类是java.lang.Object,所有Java类都有Object类的属性和方法
  • 父类要想正确初始化,需要在构造器中初始化,在子类的构造器中,Java会自动插入对父类构造器的调用;子类重写父类方法后,可以通过super关键字调用被子类覆盖的父类的原方法和属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class FatherClass {
FatherClass(){
System.out.println("FatherClass");
}

public void test(){
System.out.println("test1");
}
}

public class ChildClass extends FatherClass {
ChildClass(){ //子类构造器中,Java会自动插入对父类构造器的调用
System.out.println("ChildClass");
}

public void test(){ //重写父类中的方法test()
System.out.println("test2");
super.test(); //调用方法test()的父类版本
}

public static void main(String[] args) {
ChildClass c = new ChildClass();
c.test();
}
}
  • 如果父类没有默认构造器,或者子类想调用含有自变量的某个父类构造器,必须明确编写对父类的调用代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Game {
Game(int i){ //父类Game的构造器含有一个自变量
System.out.println("Game");
}
}

class BoardGame extends Game {
BoardGame(int i){
super(i); //注意子类调用的格式
System.out.println("BoardGame");
}
}

public class Chess extends BoardGame {
Chess(){
super(8); //注意子类调用的格式
System.out.println("Chess");
}

public static void main(String[] args) {
Chess c = new Chess();
}
}

5.3 合成与继承

  • 合成与继承都是实现类再生、代码重复利用的手段
  • 如果想利用新类内部一个现有类的特性,而不想使用它的接口,通常用合成
  • 选择继承,就需要在现有类的基础上,制作它的一个特殊版本;如果必须要上溯造型,就要用继承
  • 总之,合成表达的是“包含”关系,继承表达的是“属于”关系

5.4 final

  • final修饰变量,则变量只能被读取,不可再更改,不能被重新赋值,但仍能为其分配一个null空句柄
  • final修饰方法,则方法不可被子类重写,子类无法覆盖或改写,但可被过载;注意类中所有private方法都自动成为final
  • final修饰类,则类不能被继承,类中所有方法都默认为final

5.5 初始化 initialize

  • 首先,当前类如果有父类,Java会找到并载入父类
  • 接着,在父类执行static初始化,然后在子类中执行static初始化,(注意static初始化只会在必要的时候进行,仅发生一次,之后不会再重新初始化)至此,必要的类已全部加载完毕
  • 然后,执行main方法,开始创建对象,非static成员会初始化,先调用父类构造器,再调用子类构造器
  • 最后,执行程序的剩余部分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//初始化的整个过程
class Insect {
int i = 9;
int j;

Insect(){ //5.在初始化父类的所有数据类型后调用父类构造器
prt("i="+i+", j="+j);
j = 39;
}

static int x1 = prt("static Insect.x1 initialized"); //2.在父类Insect中执行static初始化

static int prt(String s){
System.out.println(s);
return 47;
}
}

public class Beetle extends Insect { //1.Java运行Beetle时,首先会将父类Insect载入
int k = prt("Beetle.k initialized");

Beetle(){ //6.在初始化子类的所有数据类型后调用子类构造器
prt("k="+k);
prt("j="+j);
}

static int x2 = prt("static Beetle.x2 initialized"); //3.在子类Beetle中执行static初始化

static int prt(String s){
System.out.println(s);
return 63;
}

public static void main(String[] args) { //4.执行main方法,开始创建对象
prt("Beetle constructor");
Beetle b = new Beetle();
prt("the end"); //7.最后执行剩余部分
}
}

5.6 重写(覆盖)override

  • 子类继承父类后,重写父类的方法,用自身的行为替换父类的行为
  • 重写的方法名和形参列表要相同;返回值类型和声明异常类型,子类要小于等于父类;访问权限,子类要大于等于父类
  • 注意重写与过载的区别

6 多形性(多态)polymorphism

  • 调用同一个方法,由于对象不同可能会有不同的行为
  • 多态是方法的多态,不是属性的多态
  • 多态存在的3个必要条件:继承;方法重写;父类引用指向子类对象
  • 多态这部分需要补充例证:
1
2



7 接口

7.1 抽象 abstract

  • 抽象方法:使用abstract修饰的方法,没有方法体,没有花括号,只有一个声明,便于子类方法提供具体实现
  • 抽象类:含有抽象方法的类,必须指定为abstract,抽象类仅仅表达接口,不含具体实施细节
  • 抽象类不能合成(不能用new关键字创建对象),只能被子类调用、继承,便于严格控制子类的设计,使子类之间更加通用

7.2 接口 interface

  • 接口可以理解为一个“纯”抽象类,用于建立类与类之间的一个“协议”
  • 接口和抽象的选择?使用接口,可以同时获得抽象类和接口,因此多数情况使用接口
  • 接口的访问控制符只能是public或default
  • 接口中的属性只能是常量,总是public static final修饰,不写也是
  • 接口中的方法只能是public abstract,不写也是
1
2
3
4
5
6
7
8
9
10
11
interface Interface01 { int MAX_VALUE = 100; }           //属性只能是常量,总是public static final修饰,不写也是

interface Interface02 { void fly(); } //方法只能是public abstract,不写也是

interface Interface03 extends Interface01,Interface02 {} //接口与接口之间使用extends可以实现多继承

class Class01 implements Interface01,Interface02 { //使用implements生成与接口相符的类,可以跟多个接口

@Override //必须重写接口中的所有方法,否则只能将类设为抽象类
public void fly() {}
}

8 内部类

  • 将一个类的定义放在另一个类的定义内部
1
2



9 对象的容纳

9.1 数组 array

  • 数组代表一系列对象或基本数据类型,是相同数据类型的有序集合,每一个数据称为一个元素,每个元素可以通过索引下标来访问
  • 特点:长度是确定的,一旦被创建,大小不可更改;元素必须是相同类型,不能是混合类型;数组类型可以是任何数据类型,包括基本类型和引用类型
  • 对象数组和基本数据类型数组在使用方法上几乎完全一样,唯一的差别在于对象数组容纳的是句柄,而基本数据类型数组容纳的是具体数值
  • 数组可以用作方法的返回值类型,即“返回一个数组”,此时返回的是指向数组的句柄
  • 数组的初始化,见下面示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
class User { String name; }

class Arrays {
public static void main(String[] args) {
int[] a1 = {1,2,3}; //静态初始化
int[] a2;
a2 = a1; //不是赋值,是传递句柄,a1和a2指向内存堆里同样的数组对象

for (int i=0; i<a2.length; i++) {
a2[i]++;
System.out.println("a2["+i+"]="+a2[i]);
}

for (int i=0; i<a1.length; i++) {
System.out.println("a1["+i+"]="+a1[i]);
}

User[] a3; //数组元素可以是任何数据类型
Integer[] a4; //注意Integer是一个类
int[] a5 = new int[5]; //连长度一起定义
a5[0] = 2; //通过索引下标给元素赋值

for (int i=0; i<a5.length; i++){ //通过循环给数组元素赋值(如果元素值有规律的话)
a5[i] = 10*i;
}

for (int i=0; i<a5.length; i++){ //通过循环读取元素值
System.out.println(a5[i]);
}

for (int i:a5) { //foreach用于读取数组或集合中的所有元素,不能做修改
System.out.println(i);
}

}
}

class MultiDimensionArrays {
public static void main(String[] args) {

//静态初始化一个二维数组
int[][] a1 = {
{1, 2, 3, },
{4, 5, 6, },
};

//固定大小的三维数组,可通过for嵌套循环对元素进行操作
int[][][] a2 = new int[2][3][4];
for (int i=0; i<a2.length; i++){
for (int j=0; j<a2[i].length; j++){
for (int k=0; k<a2[i][j].length; k++){
System.out.println("a2["+i+"]["+j+"]["+k+"]="+a2[i][j][k]);
}
}
}

//一开始没有完全固定大小的三维数组,通过3个new逐级固定三维的大小
int[][][] a3 = new int[2][][];
for (int i=0; i<a3.length; i++){
a3[i] = new int[3][];
for (int j=0; j<a3[i].length; j++){
a3[i][j] = new int[4];
}
}

}
}

9.2 泛型 generics

  • 通过使用泛型,预先指定容器所能保存的类型(此类的子类型也允许),可以在编译期防止将错误类型的对象放置在容器中,简单实例如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Apple {
private static long counter;
private final long id = counter++;
public long id(){ return id; }
}

class Orange {}

class ApplesAndOrangesWithGenerics {
public static void main(String[] args) {
//此处使用了泛型,<>内是类型参数,可以有多个,指定了这个容器实例所能保存的类型
ArrayList<Apple> apples = new ArrayList<>();

for (int i=0; i<3; i++){
apples.add(new Apple());
}

//有了泛型,下一句将类Orange添加到apples里面的操作在编译期就能示错;没有泛型,只有程序运行之后才能示错
//apples.add(new Orange());

for (int i=0; i<apples.size(); i++){
System.out.println(apples.get(i).id());
}
for (Apple c:apples) {
System.out.println(c.id());
}
}
}

9.3 容器(集合)Collection

  • 为容纳一组对象,首选通常是数组,但如果不知道需要多少个对象,或者保存方式比较复杂,可能会采用容器,因为容器可以自动调整大小
  • 容器只能容纳对象句柄,不能容纳基本数据类型
  • 容器包括两大类:Collection(元素独立)和 Map(含有成对的键值对,类似于字典),其中 Collection 包括List(必须按照插入的顺序保存元素)、Set(不能有重复元素)、Queue(一端插入对象,另一端移除对象)
  • 关于 List<Apple> apples = new ArrayList<>();ArrayList<Apple> apples = new ArrayList<>(); 的区别:前者apples用到的是List接口中的方法,无法调用List接口以外的方法;后者apples调用的是ArrayList类中的方法,ArrayList类是List接口的实现,有一些有别于List接口的方法;当代码写完发现需要将容器类型更改为LinkedList,对于前者,只需替换创建语句为 List<Apple> apples = new LinkedList<>(); 即可,后面的代码不需要修改,因为apples用的是List接口中的方法,同样适用于LinkedList,而对于后者,就不能直接这样替换,因为apples调用的是ArrayList类中的方法,不一定适用于LinkedList

9.4 List

  • 使用方法add()插入对象,用get()访问对象,用size()统计元素个数
  • List接口在Collection的基础上添加了大量的方法,使得可以在List中间插入和移除元素
  • ArrayList:长于随机访问元素,但在List中间插入和移除元素时较慢
  • LinkedList:随机访问较慢,长于在List中间插入和移除元素;可以直接将其作为栈使用

9.5 迭代器 iterator

  • 作用:遍历并选择序列中的对象,有了迭代器就不用再关注容器中的元素数量,由迭代器去关注
  • Iterator:只能单向移动,使用方法iterator()准备返回序列的第一个元素,next()获得序列中的下一个元素,hasNext()检查序列中是否还有元素,remove()将next()新近返回的元素删除
  • ListIterator:是迭代器的子类型,只能用于List类,可以双向移动,不光有hasNext(),还有hasPrevious(),还可用方法set()替换访问过的最后一个元素

9.6 Stack

  • 栈,是指“后进先出”的容器,最后压入的元素,最先弹出来,push()压入,pop()弹出

9.7 Set

  • Set接口与Collection完全一样,没有任何额外的功能,只是行为不同
  • Set不保存重复的元素,最被常用的是测试归属性,查询某个对象是否在Set中,比如方法contains()
  • HashSet提供最快的查询速度;TreeSet保持元素始终处于排序状态,所以没有HashSet快;LinkedHashSet保持元素插入的顺序

9.8 Map

  • 保存成对的键值对,将对象映射到其他对象,类似于字典
  • HashMap用来快速访问;TreeMap保持“键”始终处于排序状态,所以没有HashMap快;LinkedHashMap保持元素插入的顺序

9.9 Queue

  • 先进先出,从一端插入元素,从另一端取出,插入与取出的顺序相同

10 异常

10.1 异常处理程序

  • try-catch-finally:将可能会引起异常的代码集中放在try块中,用try块来捕获所有异常,缺点是try中一旦有一个异常,剩下的就不执行了,所以只能将全部异常隔离在此,不能一次性找出所有异常;抛出异常后执行相应的catch语句;finally语句总能得到执行,最多只能有一条,注意,如果在finally中使用return,那么程序即使抛出了异常,也不会有任何输出
1
2
3
4
5
6
7
8
9
try {
//可能会引起异常的代码集中在此
} catch(Type1 id1) {
//Type1的异常处理程序
} catch(Type2 id2) {
//Type2的异常处理程序
} finally {
//总能得到执行的语句
}

10.2 自定义异常

  • 自定义异常需要从已有的异常类继承(Exception或RuntimeException,若是Exception则需要用try-catch捕获异常或者throws异常说明,否则编译器会报错),最简单的方法是使用默认构造器,当然也可以定义一个接受字符串参数的构造器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class SimpleException extends Exception {}

class MyException extends Exception {
public MyException(){}
public MyException(String s){super(s);} //super调用其默认构造器,接受一个字符串作为参数
}

public class TestException {

//方法后面throws加一个异常类型列表(逗号隔开),表示异常说明,告知某个方法可能会抛出异常
public static void f1() throws SimpleException {
throw new SimpleException();
}

public static void f2() throws MyException {
throw new MyException();
}

public static void f3() throws MyException {
throw new MyException("at f3()");
}

public static void main(String[] args) {
try {
f1();
} catch (SimpleException e){
System.out.println("出错啦!");
}
try {
f2();
} catch (MyException e){
e.printStackTrace(System.out); //printStackTrace()打印从方法调用处到异常抛出处的方法调用序列
}
try {
f3();
} catch (MyException e){
e.printStackTrace(System.out);
}
}
}

10.3 捕获所有异常

  • 通过捕获异常类型的基类Exception,就可以实现一个异常处理程序捕获所有异常,注意这个作用是不遗漏任何一个异常,而不是一次性捕获所有异常(因为受限于try的顺序执行),所以可以把下面这一句catch放在异常处理程序的末尾,以达到“最后一道防线”的效果
1
catch (Exception e) { e.printStackTrace(System.out); }