static关键字
static意思是静态的、全局的,一旦被修饰,说明被修饰的东西在一定范围内是共享的,谁都可以访问,这时候需要注意并发读写的问题。
1、修饰的对象
static可以修饰成员变量、方法和代码块。
-
修饰成员变量
当static修饰成员变量时,如果该变量是public,表示该变量任何类都可以直接访问,无需初始化类,直接使用类名的方式就可以访问。这时候要特别注意线程安全的问题,当多个线程同时对共享变量进行读写时,很有可能出现并发问题,如我们定义了
public static List<String> list = new ArrayList<>()这样的共享变量,这个list如果同时被多个线程访问,就会有线程安全问题,常见的解决办法有:把线程不安全的ArrayList换成线程安全的CopyOnWriteArrayList;每次访问时手动加锁(可以使用synchronized和ReentranctLock,不能使用volite,因为volite不是原子性的,无法保证线程安全)。 -
修饰方法
当static修饰方法时,代表该方法和当前类无关,任意类都可以直接访问(前提是方法是public的)。有一点需要注意,该方法内部只能调用同样被static修饰的方法和成员变量。static内部的方法在执行的时候是没有线程安全问题的,因为方法执行时,数据是运行在栈帧里面的,栈的数据每个线程都是隔离开的,所以不会有线程安全问题,所以我们在编写util类时,static方法是可以放心使用的。
-
修饰代码块
当static修饰代码块时(也叫静态块),常常用于在类启动之前,对类进行一些初始化的操作。需要注意的是,静态块只能调用同样被static修饰的变量,并且static变量要写在静态块前面,不然编译会报错
2、初始化的时机
对于被static修饰的类变量、方法块和静态方法的初始化时机
class ParentClass {
public static List LIST = new ArrayList(){{
System.out.println("父类静态变量初始化");
}};
static {
System.out.println("父类静态块初始化");
}
public ParentClass() {
System.out.println("父类构造方法初始化");
}
public static void testStatic() {
System.out.println("父类静态方法初始化");
}
}
public class ChildrenClass extends ParentClass {
public static List LIST = new ArrayList(){{
System.out.println("子类静态变量初始化");
}};
static {
System.out.println("子类静态块初始化");
}
public ChildrenClass() {
System.out.println("子类构造方法初始化");
}
public static void testStatic() {
System.out.println("子类静态方法初始化");
}
public static void main(String[] args) {
System.out.println("run main");
new ChildrenClass();
System.out.println("########");
ChildrenClass.testStatic();
}
}
/**
* 运行结果:
* 父类静态变量初始化
* 父类静态块初始化
* 子类静态变量初始化
* 子类静态块初始化
* run main
* 父类构造方法初始化
* 子类构造方法初始化
* ########
* 子类静态方法初始化
*/
从结果中可以看出:
- 父类的静态变量和静态块比子类优先初始化
- 静态变量和静态块比类构造器优先初始化
- 被static修饰的方法,在类初始化时并不会初始化,只有当自己被调用时才会被执行
final关键字
final的意思是不变的,可以用来修饰类、方法、变量(成员变量、局部变量、形参)
- 被final修饰的类,表明该类无法被继承
- 被final修饰的方法,表明该方法无法被重写
- 被final修饰的变量,变量在声明的时候就必须进行初始化,而且以后也不能修改其内存地址
注意第三点,说的是无法修改内存地址,而不是无法修改其值,因为对于List、Map这些集合类来说,被final修饰后,是可以修改其内部值的,但是无法修改其初始化时的内存地址。
try、catch、finally关键字
try用来确定代码执行的范围,catch捕捉可能会发生的异常,finally用来执行一定要执行的代码块
public static void main(String[] args) {
try {
System.out.println("try is run");
throw new RuntimeException("try exception");
} catch (Exception e) {
System.out.println("catch is run");
throw new RuntimeException("catch exception");
} finally {
System.out.println("finally is run");
// throw new RuntimeException("finally exception");
}
}
/**
* 运行结果
* try is run
* catch is run
* finally is run
* Exception in thread "main" java.lang.RuntimeException: catch exception
* at io.renren.Test.main(Test.java:10)
*/
代码的执行顺序为:try -> catch -> finally
从执行结果可以看出:
- finally先执行后,再打印catch的异常(如果finally中出现异常,就抛出finally中的异常,方法结束)
- 最终捕获的是catch的异常(finally中没有抛出异常),try中抛出的异常会被catch捕获,所以我们可以在catch中封装异常信息
volatie关键字
volatile的意思是可见的,常用来修饰某个共享变量,意思是当共享变量的值被修改后,会及时通知到其他线程上,其他线程就能知道当前共享变量的值已经被修改了。
在多核CPU下,为了提高效率,线程在拿值时,是直接和CPU缓存打交道的,而不是内存,主要是因为CPU缓存执行速度更快,比如线程要拿值C,会直接从CPU缓存中拿,如果CPU缓存中没有,就会从内存中拿,然后放到CPU缓存中,所以线程读的操作永远都是拿的CPU缓存的值。这时候就会产生一个问题,CPU缓存中的值和内存中的值可能并不是时刻都同步的,导致线程计算的值可能不是最新的,共享变量的值有可能已经被其他线程所修改了,但此时修改的是机器内存的值,CPU缓存的值还是老的,导致计算会出现问题。这个时候有个机制,就是内存会主动通知CPU缓存,当前共享变量的值已经失效了,需要重写拉取一份,CPU缓存就会重新从内存中拿取一份最新的值,volatile关键字就会触发这种机制。加了volatile关键字的变量,就会被识别成共享变量,内存中值被修改后,会通知到各个CPU缓存,使CPU缓存中的值也对应被修改,从而保证线程从CPU缓存中拿取出来的值是最新的。如下图所示:

从图中可以看到,线程一和线程二一开始都读取了C值,CPU1和CPU2中也都缓存了C值,然后线程1把C值修改了,这时候内存的值和CPU2缓存中的值就不相等了,内存这时发现C值被volatile修饰了,发现其是共享变量,就会将CPU2缓存中的C值状态置为无效,当线程2从CPU2缓存中获取C值时,就会去内存中拉取最新的C值
transient关键字
transient的意思是短暂的,transient关键字只能用来修饰类的成员变量,不能修饰类和方法,作用是在对象序列化时忽略该变量。
要理解transient的作用,首先要理解序列化的含义,Java中对象的序列化指的是将对象转换成以字节序列的形式来表示,这些字节序列包含了对象的数据和信息,一个序列化后的对象可以被写到磁盘中(例如数据库或文件),也可用于网络传输。一般地,当我们使用缓存cache(内存空间不够有可能会本地存储到硬盘)或远程调用rpc(网络传输)的时候,经常需要让实体类实现Serializable接口,目的就是为了让其可序列化。当然,序列化后的最终目的是为了反序列化,恢复成原先的Java对象实例。所以序列化后的字节序列都是可以恢复成Java对象的,这个过程就是反序列化。
在持久化对象时,对于一些特殊的数据成员(如用户的密码,银行卡号等),为了安全起见,不想用序列化机制来保存它。为了在一个特定对象的一个成员变量上关闭序列化,可以在这个成员变量前加上关键字transient。
@Data
class User implements Serializable {
private String a = "普通成员变量";
private transient String b = "transient修饰的变量";
}
public class Test {
public static void main(String[] args) {
try(ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("c://test.txt"))) {
os.writeObject(new User());
os.flush();
} catch (Exception e) {
e.printStackTrace();
}
try(ObjectInputStream is = new ObjectInputStream(new FileInputStream("c://test.txt"))) {
User user = (User) is.readObject();
System.out.println("a: " + user.getA());
System.out.println("b: " + user.getC());
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 运行结果:
* a: 普通成员变量
* b: null
*/
注意:
- 如果对象需要被序列化,则类必须实现Serializable接口;
- 一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法被访问,值为null;
- 一个静态变量不管是否被transient修饰,均不能被序列化,因为序列化保存的是对象状态,而静态变量是类所有的,因此序列化不会保存静态变量;
default关键字
default 关键字一般会用在接口的方法上,意思是对于接口的该方法,子类是无需强制实现的,但自己必须有默认实现,例如:
interface Parent {
default void sayHello() {
System.out.println("parent: say hello");
}
void sayBye();
}
// 子类可以不实现接口中的被deafult关键字修饰的方法
class Children implements Parent {
@Override
public void sayBye() {
System.out.println("children: say bye");
}
}
public class Test {
public static void main(String[] args) {
Children children = new Children();
children.sayHello();
children.sayBye();
}
}
/**
* 运行结果:
* parent: say hello
* children: say bye
*/
native关键字
native关键字用于修饰方法,表明该方法是一个原生态方法,方法对应的实现不是在当前文件,而是在用其他语言(如C和C++)实现的文件中。例如Java中的System.arrayCopy()、Object.hashCode()等

776

被折叠的 条评论
为什么被折叠?



