Java 基础 - 知识点
# Java 基础知识
# 1 数据类型
# 1.1 基本数据类型
8 种基本数据类型:
- byte:1 字节(8位),默认值 0,-128 ~ 127
- short:2 字节(16位),默认值 0,-32768 ~ 32767
- int:4 字节(32位),默认值 0,-2147483648 ~ 2147483647
- long:8字节(64位),默认值 0L,-9223372036854775808 ~ 9223372036854775807
- float:4字节(32位),默认值 0.0f,单精度,大约 +-3.40282347E+38F(有效十进制位数为6~7位)
- double:8字节(64位),默认值 0.0d,双精度,大约 +-1.79769313486231570E+-308(有效十进制位数为15位)
- char:一个单一的 16 位 Unicode 字符,默认值'\u0000'(null), '\u0000' ~ '\uffff' 即 0 ~ 65535
- boolean:默认值 false,true/false
当类的某个成员属性是基本类型,即使没有进行初始化,Java 也会确保它获得一个默认值。
# 1.2 包装类
包装类为基本类型 int 添加了属性和方法,丰富了基本类型的操作。
Java 为 8 种基本数据类型提供了 8 种对应的包装类, 一切都是对象:
- Byte (byte)
- Short (short)
- Integer (int)
- Long(long)
- Float (float)
- Double (double)
- Character (char)
- Boolean (boolean)
# 1.2.1 缓存
Java 为 6 种基本数据类型提供的缓存范围:
- Byte 全部缓存,缓存所有, -128 到 127 数值
- Short 缓存 -128 到 127 数值
- Integer 默认缓存 -128 到 127 数值,可设置最大缓存值
- Long 缓存 -128 到 127 数值
- Character,缓存范围 ’\u0000’ 到 ‘\u007F’
- Boolean 缓存 true/false 对应实例,确切说,只会返回两个常量实例 Boolean.TRUE/FALSE
以 Integer 包装类为例:
在 Java 5 进行了另一个改进: Integer 值缓存。通常构建 Integer 对象是调用构造函数 new Integer(1)
,直接 new 一个对象。但实际中发现大部分的数据操作都是集中在有限的、较小数值范围。因而 Java 5 中在 Integer 中新增了 valueOf() 静态方法,将某范围内的值缓存,之后直接从缓存中获取,无需创建新的对象。带来明显的性能改进。
由于存在缓存机制,所以在进行数值比较时可能出现:额
Integer a1 = 100; // 会自动编译为:Integer.valueOf(100)
Integer a2 = Integer.valueOf(100);
a1 == a2; // 结果为 true;
Integer b1 = Integer.valueOf(200);
Integer b2 = Integer.valueOf(200);
b1 = b2; // 结果为 false
Integer c1 = new Integer(100);
Integer c2 = Integer.valueOf(100);
c1 == c2; // 结果为 false;c1 是 new 出来的对象是在堆中开辟了新地址,所以地址不相同,
2
3
4
5
6
7
8
9
10
11
# 1.2.2 自动拆箱和装箱
在 Java 5 中,引入自动装箱和自动拆箱语法糖(boxing / unboxing),Java 在编译阶段可以根据上下文,自动进行装拆箱,即生成相同的字节码,极大简化了相关编程。
以 Integer 包装类为例:int 到 Integer 和 Integer 到 int
Integer boxing = 1; // 实际会自动转换成 Integer.valueOf(1); int 自动装箱为 Integer 包装类
int unboxing = integer++; // 实际会自动转换成 (integer ++).intValue(); Integer 自动拆箱为 int 基本类型
2
# 1.3 扩展
# 2 String 字符串
# 2.1 不可变
Java 中 String 是不可变的。实现方式:String 类使用关键字 final 修饰,且保存字符串内容的字符数组 value 也使用关键字 final 修饰,一旦初始化就不可变。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
...
}
2
3
4
5
6
当需要改变字符串内容时,String 类的方法都会返回一个新的 String 对象;如果没有变化则只返回指向原对象的引用。
public class Imutable {
public static String upcase(String s) {
return s.toUpperCase();
}
public static void main(String[] args) {
String q = "howdy";
System.out.println(q); // howdy
String qq = upcase(q);
System.out.println(q); // howdy
System.out.println(qq); // HOWDY
}
}
2
3
4
5
6
7
8
9
10
11
12
13
如上代码中,当把 q 传递给 upcase 方法时,实际传递的是引用的一个拷贝,引用别名为 s ,当方法 upcase 执行时才存在参数 s,在执行完后被销毁。upcase 方法返回了一个新的 String 对象。
# 2.2 String 设计为不可变的原因
参考:Why String is Immutable in Java? (opens new window)
String Pool (缓存池)
字符串是使用最广泛的数据结构,如果减少重复创建相同内容的字符串对象,可以有效降低内存消耗和对象创建开销。从而设计了 String Pool ,即缓存 String 对象字面值,由于 String 在 Java 中是不可变的,JVM 通过在池中仅存储相同 String 字面值的一个副本即可。即相同字面值的字符串变量的引用是同一个对象。
String s1 = "hello world"; String s2 = "hello world"; String s3 = new String("hello world"); System.out.println(s1 == s2); // true System.out.println(s1 == s3); // false
1
2
3
4
5
6Security(安全性)
Java 中 String 广泛用于存储敏感信息,如用户名、密码、网络连接等,比如 String 常常作为方法参数传递,如下面的代码:
从不可信的调用者接收字符串参数 userName,对 userName 进行安全性检查后用于数据库更新操作。如果字符串是可变的,不可信的调用者也引用该字符串对象,随时可以变更字符串的值,导致 SQL 注入的情况。
void criticalMethod(String userName) { // perform security checks if (!isAlphaNumeric(userName)) { throw new SecurityException(); } // do some secondary tasks initializeDatabase(); // critical task connection.executeUpdate("UPDATE Customers SET Status = 'Active' " + " WHERE UserName = '" + userName + "'"); }
1
2
3
4
5
6
7
8
9
10
11
12
13Synchronization(线程安全)
由于不可变性,使得 String 是线程安全的,即在多线程访问时,无法被改变,可在同时运行的多个线程之间共享。
Hashcode Caching
由于 String 常常会被应用于哈希实现,如 HashMap、HashTable、HashSet 的 Key 值,而 hashCode() 方法经常被调用,由于 String 的不可变性,使得 hashCode() 也是相同的,这样在第一次 hashCode() 调用期间计算并缓存哈希,并且从那以后返回相同的值即可,即只需要计算一次哈希值,提高了性能。
# 2.3 扩展
String、StringBuilder、StringBuffer
Java String Pool (opens new window)
# 3 操作符
Java 操作符接受一个或多个参数,并生成一个新值,有些炒作可能会改变操作数自身的值,这被称为“副作用”。
# 3.1 优先级
最简单的规则就是先乘除后加减,建议使用括号明确规定计算顺序。
# 3.2 赋值
赋值使用操作符“=”。意思是“取右边的值,把它复制给左边”。
基本数据类型存储了实际的数值,而并非执行一个对象的引用,所以在为其赋值的时候,是直接将一个地方的内容复制到另一个地方。如:
int a,b;
a = 10;
b = a;
a = 20;
// 此时 a = 20; b=10;
2
3
4
5
对一个对象进行赋值操作的时候,真正操作的是对对象的引用。所以“将一个对象赋值给另一个对象”,实际是将“引用”从一个地方复制到另一个地方。如:
class Tank {
int level;
}
public class Assignment {
Tank t1 = new Tank();
Tank t2 = new Tank();
t1.level = 9;
t2.level = 47;
print("t1 level:" + t1.level); // t1 level:9
print("t2 level:" + t2.level); // t2 level:47
t1 = t2; // t1 原来的引用被覆盖,不再被引用的对象会由垃圾回收器自动清理
print("t1 level:" + t1.level); // t1 level:47
print("t2 level:" + t2.level); // t2 level:47
t1.level = 27;
print("t1 level:" + t1.level); // t1 level:27
print("t2 level:" + t2.level); // t2 level:47
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 3.3 参数传递
Java 的参数是以值传递的形式传入方法中,而不是引用传递。
以下代码中 Dog dog 的 dog 是一个指针,存储的是对象的地址。在将一个参数传入一个方法时,本质上是将对象的地址以值的方式传递到形参中。因此在方法中改变指针引用的对象,那么这两个指针此时指向的是完全不同的对象,一方改变其所指向对象的内容对另一方没有影响。
public class Dog {
String name;
Dog(String name) {
this.name = name;
}
String getName() {
return this.name;
}
void setName(String name) {
this.name = name;
}
String getObjectAddress() {
return super.toString();
}
}
public class PassByValueExample {
public static void main(String[] args) {
Dog dog = new Dog("A");
System.out.println(dog.getObjectAddress()); // Dog@4554617c
func(dog);
System.out.println(dog.getObjectAddress()); // Dog@4554617c
System.out.println(dog.getName()); // A
}
private static void func(Dog dog) {
System.out.println(dog.getObjectAddress()); // Dog@4554617c
dog = new Dog("B");
System.out.println(dog.getObjectAddress()); // Dog@74a14482
System.out.println(dog.getName()); // B
}
}
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
但是如果在方法中改变对象的字段值会改变原对象该字段值,因为改变的是同一个地址指向的内容:
class Letter {
char c;
}
public class PassObject {
static void f(Letter y) {
y.c = 'z';
}
public static void main(String[] args) {
Letter x = new Letter();
x.c = 'a';
print("x.c: " + x.c); // x.c: a
f(x);
print("x.c: " + x.c); // x.c: z
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
方法 f(x)
参数传递了一个引用,所以在方法内修改参数对象,实际改变的是方法之外的对象。
# 3.4 算术操作符
加号(+)、减号(-)、除号(/)、乘号(*)、取模(%)。整数除法会直接去掉结果的小数位,而不是四舍五入。
同时进行运算和赋值操作:
int x;
x += 4; // 等同于 x = x + 4
2
# 3.5 自动递增和递减
对于前缀递增和前缀递减(如 ++a 或 --a),会先执行运算,再生产值;而对于后缀递增和后缀递减(如 a++ 或 a--),会先生成值,再执行运算。
public static void main(String[] args) {
int i = 1;
print("i: " + i); // i: 1
print("++i: " + ++i); // ++i: 2
print("i++: " + i++); // ++i: 2
print("i: " + i); // i: 3
print("--i: " + --i); // --i: 2
print("i--: " + i--); // i--: 2
print("i: " + i); // i: 1
}
2
3
4
5
6
7
8
9
10
# 3.6 关系操作符
关系操作符包括小于(<)、大于(>)、小于或等于(<=)、大于或等于(>=)、等于(==)以及不等于(!=)。等于和不等于适用于所有基本数据类型,而其他比较复杂不适用于 boolean 类型。
关系操作符 == 和 != 也适用于所有对象,但比较的是对象的引用。要想比较对象的实际内容是否相同,必须使用所有对象都适用的特殊方法 equals()
public static void main(String[] args) {
Integer a = new Integer(47);
Integer b = new Integer(47);
System.out.println(a == b) // false
System.out.println(a != b); // true
System.out.println(a.equals(b); // true
}
2
3
4
5
6
7
如果使用 equals 方法比较自己的类时,需要根据实际情况重写 equals 方法。因为 equals 方法默认是比较引用。
# 3.7 逻辑操作符
逻辑操作符 与(&&)、或(||)、非(!)能根据参数的逻辑关系,生成一个布尔值 true 或 false。
当使用逻辑操作符是,会遇到一种”短路“现象。一旦能够明确无误地确定整个表达式的值,就不再计算表达式余下部分。
// 如果 test(0) 返回 false;则 a = false,不会再执行 test(1) 和 test(2)
boolean a = test(0) && test(1) && test(2);
2
# 3.8 三元操作符
三元操作符也称为条件操作符,引入目的是提高编程效率,但若频繁使用容易产生可读性差问题。
boolean-exp ? value0 : value1;
# 3.9 字符串操作符 + 和 +=
如果表达式以一个字符串起头,那么后续所有操作数都必须是字符串型。
public static void main(String[] args) {
int x = 0, y = 1, z = 2;
String s= "x, y, z ";
print(s + x + y + z); // x, y, z 012
print(x + " " + s); // 0 x, y, z
s += "(summary) = ";
print(s + (x + y + z)); // x, y, z (summary) = 3
print("" + x); // 0
}
2
3
4
5
6
7
8
9
# 3.10 类型转换
- 如果两个操作数中有一个是 double 类型,另一个操作数就会转换为 double 类型
- 否则,如果其中一个操作数是 float 类型,另一个操作数将会转换为 float 类型
- 否则,如果其中一个操作数是 long 类型,另一个操作数将会转换为 long 类型
- 否则,两个操作数都将被转换为 int 类型
double 或 float 类型转换为 int 时,总是对数字执行截尾,如果想要得到舍入结果,需要使用 java.lang.Math
中的 round() 方法。
有时需要将 double 或 long 数字转换为 int 类型,由于存在面临信息丢失的情况,需要通过强制类型转换实现:
double x = 9.997;
int nx = (int) Math.round(x); // 10。Math.round(x) 返回 long 类型;
2
# 3.11 扩展
# 4 控制执行流程
# 4.1 if-else
If-else 语句是控制程序流程的最基本形式,其中的 else 是可选的。
if (Boolean-expression) {
statement;
}
if (Boolean-expression) {
statement;
} else {
statement;
}
2
3
4
5
6
7
8
9
Boolean-expression 布尔表达式必须产生一个布尔结果,true 或 false。
# 4.2 迭代
# 4.2.1 while 与 do-while
statement 语句会重复执行,直到起控制作用的布尔表达式 Boolean-expression 得到 false 值为止。
while (Boolean-expression) {
statement;
}
do {
statement;
} while (Boolean-expression);
2
3
4
5
6
7
8
9
while 和 do-while 唯一的不同之处在于 do-while 的 statement 语句一定会执行一次,即便布尔表达式第一次结果为 false;而 while 循环结构,如果布尔表达式第一次为 false,则 statement 语句根本不会被执行。
无穷循环:while(true)
# 4.2.2 for
for 循环在第一次迭代前要进行初始化,随后进行布尔表达式测试,如果布尔表达式结果为 true,则执行 statement 语句,在 statement 语句执行结束时,进行某种形式的 ”进步“ 操作。如果布尔表达式结果为 false,则结束整个循环。
for (initialzation; Boolean-expression; step) {
statement;
}
// 变量 i 的作用域为 for 循环
for (int i = 1; i < 10; i++) {
System.out.println(i);
}
2
3
4
5
6
7
8
逗号操作符:只有 for 循环中才能使用;使用逗号操作符定义多个变量,但变量类型必须相同
for (int i= 1, j = i + 10; i < 5; i++, j = i * 2) {
System.out.println("i = " + i + " j = " + j);
}
2
3
无穷循环:for(;;)
# 4.2.3 Foreach
Java 5 引入了一种新的更加简洁的 for 语句,用于遍历数组和容器
List<String> stringList = new ArrayList();
stringList.add("a");
stringList.add("b");
stringList.add("c");
for (String item : stringList) {
System.out.println(item);
}
2
3
4
5
6
7
8
# 4.3 return
两方面用途:
- 指定一个方法返回什么值,假设它没有 void
- 导致当前的方法退出,并返回那个值
在返回 void 的方法中没有 return 语句,那么该方法的结尾会有一个隐式的 return
# 4.4 break 和 continue
区别:
- break 用于退出整个循环,不再执行循环的语句;
- continue 则用于停止当前的迭代,然后退回到循环起始位置,然后执行下一个迭代
# 4.5 switch
switch 是实现多路选择的一种干净利落的方法。根据整数表达式的值,switch 语句可以从一系列代码中选出一段去执行。
switch (integral-selector) {
case integral-value1: statement; break;
case integral-value2: statement; break;
case integral-value3: statement; break;
default: statement;
}
2
3
4
5
6
integral-selector 整数表达因子(必须是 int 或 char 或 enum),是一个能够产生整数数值的表达式,将结果与每个 case 的 integral-value 进行比较,如果符合则执行相应的 statement;如果没有相符的,则执行 default 的 statement;
case 的 break 是可选的,如果没有则会继续执行后面的 case 语句。
# 5 数组
# 5.1 数组定义
数组是一种数据结构,用来存储统一数据类型值的集合。通过整型下标可以访问数组的每一个值。
无论使用哪种类型的数组,数值标识符其实只是一个引用,指向在堆中创建的一个真实的对象,这个(数组)对象用以保存指向其他对象的引用
声明数组:int[] a
或 int a[]
,通常使用第一种方式。
初始化数组:int[] a = new int[100]
,该语句创建了一个可以存储 100 个整数的数组。
注意:
- 创建一个数值数组时,所有元素都初始化为 0;char 数组的元素初始化为 (char)0;boolean 数组的元素初始化为 false;对象数组的元素初始化为一个特素值 null,表示还未存放任何对象;
- 数组 length 表示数组的大小,而不是实际保存元素的个数
一旦创建了数组,就不能再改变它的大小。如需在运行过程中扩展数组大小,则应使用 ArrayList 列表
# 5.2 数组初始化及匿名数组
// 创建对象,再初始化
int[] a = new int[2];
a[0] = 2;
a[1] = 3;
// 创建对象且同时初始化,数组大小就是初始值的个数
int[] smallPrimes = {2, 3};
// 匿名数组, 重新初始化一个数组
smallPrimes = new int[] {2, 3, 13};
2
3
4
5
6
7
8
9
# 5.3 数组遍历
使用 for 或 foreach 语句遍历数组:
for (int i= 0; i < a.length; i++) {
System.out.println(a[i]);
}
for (int item : a) {
System.out.println(item);
}
2
3
4
5
6
7
# 5.4 数组拷贝
int[] smallPrimes = {2, 3};
int[] luckNumbers = smallPrimes;
luckNumbers[1] = 13;
System.out.println(Arrays.toString(smallPrimes)); // [2, 13]
System.out.println(Arrays.toString(luckNumbers)); // [2, 13]
2
3
4
5
luckNumbers = smallPrimes
使得两个变量引用同一个数组,并没有创建新的数组。
如果想创建新的数组则使用 Arrays.copyOf()
方法,如果第二个参数值小于原始数组长度,则只拷贝最前面的数据元素:
int[] smallPrimes = {2, 3};
int[] luckNumbers = Arrays.copyOf(smallPrimes, smallPrimes.length);
System.out.println(smallPrimes == luckNumbers); // false
System.out.println(Arrays.toString(smallPrimes)); // [2, 3]
System.out.println(Arrays.toString(luckNumbers)); // [2, 3]
luckNumbers[0] = 12;
System.out.println(Arrays.toString(smallPrimes)); // [2, 3]
System.out.println(Arrays.toString(luckNumbers)); // [12, 3]
2
3
4
5
6
7
8
Arrays.copyOf()
方法使用了 System.arraycopy()
方法实现数组复制。System.arraycopy()
不会执行自动包装和自动拆包,所以两个数组必须具有相同的确切类型。
public static int[] copyOf(int[] original, int newLength) {
int[] copy = new int[newLength];
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
2
3
4
5
6
注意:使用 System.arraycopy()
方法复制数组,当复制的是对象数组,只是复制了对象的引用,而不是对象本身的拷贝。称为 浅复制 或 浅拷贝
# 5.5 数组的比较
Arrays 类为不同数据类型提供了重载后的 equals()
方法,用来比较整个数组。
数组相等的条件:元素个数必须相等;并且对应位置的元素也相等;
比较数组元素是否相等,是使用元素的 equals()
方法。对于基本类型,需要使用基本类型的包装器类的 equals()
方法;如果是自建的类对象,则可以通过重写类 equals()
方法实现比较内容是否相等,而非父类 Object 默认的 equals(只比较引用是否相等)
# 5.6 数组排序
排序必须根据对象的实际类型执行比较操作。
程序设计的目标是:“将保持不变的事物与会发生变化的事物相分离”,而数组排序不变的是通用的排序算法,变化的是各种对象相互比较的方式。通过策略模式将“会发生变化的代码”封装在单独的类中(策略对象),可以将策略对象传递给总是相同的排序代码完成其排序算法。
Arrays 类为基本数组类型分别重载了 sort() 方法,使用 Dual-Pivot 快排(双轴快速排序算法) 或 并行排序算法 实现数组的排序功能;
Java 为对象数组排序提供两种方式来实现比较功能。
public class Arrays {
private static final int MIN_ARRAY_SORT_GRAN = 1 << 13;
// 第一种
public static void sort(Object[] a) {
if (LegacyMergeSort.userRequested)
// 兼容 1.6 之前的旧版本,采用冒泡排序和归并排序
legacyMergeSort(a);
else
ComparableTimSort.sort(a, 0, a.length, null, 0, 0);
}
// 第二种
// 参数 a 没有实现 Comparable 接口;所以需要第二个参数接收 Comparator 策略
public static <T> void sort(T[] a, Comparator<? super T> c) {
if (c == null) {
sort(a);
} else {
if (LegacyMergeSort.userRequested)
legacyMergeSort(a, c);
else
TimSort.sort(a, 0, a.length, c, null, 0, 0);
}
}
...
// 双轴快速排序算法(单线程)O(n log(n))
public static void sort(int[] a) {
DualPivotQuicksort.sort(a, 0, a.length - 1, null, 0, 0);
}
...
// 如果小于 MIN_ARRAY_SORT_GRAN 2的13次方即8192, 使用单线程的双轴快速排序算法
// 否则使用并行排序算法
public static void parallelSort(byte[] a) {
int n = a.length, p, g;
if (n <= MIN_ARRAY_SORT_GRAN ||
(p = ForkJoinPool.getCommonPoolParallelism()) == 1)
DualPivotQuicksort.sort(a, 0, n - 1);
else
new ArraysParallelSortHelpers.FJByte.Sorter
(null, a, new byte[n], 0, n, 0,
((g = n / (p << 2)) <= MIN_ARRAY_SORT_GRAN) ?
MIN_ARRAY_SORT_GRAN : g).invoke();
}
...
}
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
实现
java.lang.Comparable
接口,该接口只用一个 compareTo() 方法,此方法接收一个 Object 类型的参数。如果当前对象小于它则返回 -1;如果相等则返回 0;如果大于返回 1;常见类都实现了
java.lang.Comparable
接口,如果基本类型的包装类:public final class Integer extends Number implements Comparable<Integer> { ... public int compareTo(Integer anotherInteger) { return compare(this.value, anotherInteger.value); } public static int compare(int x, int y) { return (x < y) ? -1 : ((x == y) ? 0 : 1); } ... }
1
2
3
4
5
6
7
8
9
10
11对象数组则调用
sort(Object[] a)
方法,ComparableTimSort(与 TimSort 算法一样)直接利用的是实现了Comparable 接口的对象来进行比较操作。即 Object[] a 必须实现 Comparable 接口,否则将抛出 ClassCastException创建一个单独的类实现
java.util.Comparator
作为比较策略,调用 Arrays 的sort(T[] a, Comparator<? super T> c)
并将传递比较策略。Comparator 有两个方法 compare() 和 equals()
# 5.7 数组与泛型
class ClassParameter<T> {
public T[] f(T[] arg) { return arg; }
}
class MethodParameter {
public static <T> T[] f(T[] arg) { return arg; }
}
public class ParameterizedArrayType {
public static void main(String[] args) {
Integer[] ints = {1, 2, 3, 4, 5};
Double[] doubles = {1.1, 2.2, 3.3, 4.4, 5.5};
Integer[] ints2 = new ClassParameter<Integer>().f(ints);
Double[] doubles2 = new ClassParameter<Double>().f(doubles);
ints2 = MethodParameter.f(ints);
doubles2 = MethodParameter.f(doubles);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 6 枚举
# 7 关键字
# static
- 静态只能访问静态
- 非静态既可以访问非静态,也可以访问静态
# 静态变量
- 静态变量在类被初次加载时初始化
- 静态变量在类的任何对象实例化前被初始化
- 静态变量在类的任何静态方法执行之前被初始化
**被 static 修饰的类变量独立于该类的任何对象,而是被所有类的对象所共享。**即 static 变量值在类加载的时候分配空间,优先于类的实例对象,在内存中只有一个副本。而非 static 变量是在类实例化对象时初始化,每个对象都有一个副本。
class StaticTest {
static int i = 47;
}
2
3
即使创建两个 StaticTest 对象,StaticTest.i
也只有一份存储空间,这两个对象共享一个 i
StaticTest st1 = new StaticTest();
StaticTest st2 = new StaticTest();
st1.i == st2.i; // true
StaticTest.i++;
st1.i == 48; // true
st2.i == 48; // true
2
3
4
5
6
使用类名是引用 static 变量的首选方式,不仅强调变量的 static 结构,而且在某些情况有利于编译器进行优化。
# 静态方法
- 静态方法在类被初次加载时初始化
- 静态方法属于类而不是类的对象,在不创建任何对象是调用
- 静态方法中只能访问静态变量和静态方法
- 静态方法中不能使用 this 或 super 关键字
- 抽象方法不能是静态的
- 静态方法不能被覆盖
static 方法的一个重要用法就是在不创建任何对象的前提下就可以调用它。在静态方法中不能访问非静态变量和非静态方法(它们都依赖于具体的对象才能够被调用)。
class Incrementable {
static void increment() {
StaticTeset.i++;
}
}
2
3
4
5
在单例模式中常常使用静态方法来创建类的实例化对象。或一些工具类如:Arrays.sort()
、Math.random()
# 静态代码块
static关键字还有一个比较关键的作用就是 用来形成静态代码块以优化程序性能。static 块可以置于类中的任何地方,类中可以有多个 static 块。在类初次被加载的时候,会按照 static 块的顺序来执行每个 static 块,并且只会执行一次。很多时候会将一些只需要进行一次的初始化操作都放在 static 代码块中进行。
class Test {
static Map<Integer, String> month;
static {
month = new HashMap<>();
month.put(1, "一月");
month.put(2, "二月");
month.put(3, "三月");
...
}
}
2
3
4
5
6
7
8
9
10
# 静态嵌套类
- 在 Java 中,外部类不能是静态的,嵌套类才能是静态类
- 静态嵌套类不需要引用外部类的内容
- 静态嵌套类不能访问外部类的非静态成员
将只在一个地方使用的类嵌套在一个类中,增加了封装性、可读性、可维护性。
Java 源码中 static 关键字经常被使用,如 Integer 源码:
public final class Integer extends Number implements Comparable<Integer> {
@Native public static final int MIN_VALUE = 0x80000000;
@Native public static final int MAX_VALUE = 0x7fffffff;
...
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
...
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
// 初始化缓存
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
}
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
由上面源码可见,static 关键字通常与 final 关键字同时使用。