Java 是很多人一直在用的编程语言,但是有些 Java 概念是非常难以理解的,哪怕是一些多年的老手,对某些 Java 概念也存在一些混淆和困惑。
所以,在这篇文章里,会介绍四个 Java 中最难理解的四个概念,去帮助开发者更清晰的理解这些概念:
- 匿名内部类的用法
- 多线程
- 如何实现同步
- 序列化
匿名内部类
匿名内部类又叫匿名类,它有点像局部类(Local Class)或者内部类(Inner Class),只是匿名内部类没有名字,我们可以同时声明并实例化一个匿名内部类。
一个匿名内部类仅适用在想使用一个局部类并且只会使用这个局部类一次的场景。
匿名内部类是没有需要明确声明的构造函数的,但是会有一个隐藏的自动声明的构造函数。
创建匿名内部类有两种办法:
- 通过继承一个类(具体或者抽象都可以)去创建出匿名内部类
- 通过实现一个接口创建出匿名内部类
咱们看看下面的例子:
// 接口:程序员
interface Programmer {
void develop();
}
public class TestAnonymousClass {
public static Programmer programmer = new Programmer() {
@Override
public void develop() {
System.out.println("我是在类中实现了接口的匿名内部类");
}
};
public static void main(String[] args) {
Programmer anotherProgrammer = new Programmer() {
@Override
public void develop() {
System.out.println("我是在方法中实现了接口的匿名内部类");
}
};
TestAnonymousClass.programmer.develop();
anotherProgrammer.develop();
}
}
从上面的例子可以看出,匿名类既可以在类中也可以在方法中被创建。
之前我们也提及匿名类既可以继承一个具体类或者抽象类,也可以实现一个接口。所以在上面的代码里,我创建了一个叫做 Programmer 的接口,并在 TestAnonymousClass 这个类中和 main() 方法中分别实现了接口。
Programmer除了接口以外既可以是一个抽象类也可以是一个具体类。
抽象类,像下面的代码一样:
public abstract class Programmer {
public abstract void develop();
}
具体类代码如下:
public class Programmer {
public void develop() {
System.out.println("我是一个具体类");
}
}
OK,继续深入,那么如果 Programmer 这个类没有无参构造函数怎么办?我们可以在匿名类中访问类变量吗?我们如果继承一个类,需要在匿名类中实现所有方法吗?
public class Programmer {
protected int age;
public Programmer(int age) {
this.age = age;
}
public void showAge() {
System.out.println("年龄:" + age);
}
public void develop() {
System.out.println("开发中……除了异性,他人勿扰");
}
public static void main(String[] args) {
Programmer programmer = new Programmer(38) {
@Override
public void showAge() {
System.out.println("在匿名类中的showAge方法:" + age);
}
};
programmer.showAge();
}
}
- 构造匿名类时,我们可以使用任何构造函数。上面的代码可以看到我们使用了带参数的构造函数。
- 匿名类可以继承具体类或者抽象类,也能实现接口。所以访问修饰符规则同普通类是一样的。子类可以访问父类中的 protected 限制的属性,但是无法访问 private 限制的属性。
- 如果匿名类继承了具体类,比如上面代码中的 Programmer 类,那么就不必重写所有方法。但是如果匿名类继承了一个抽象类或者实现了一个接口,那么这个匿名类就必须实现所有没有实现的抽象方法。
- 在一个匿名内部类中你不能使用静态初始化,也没办法添加静态变量。
- 匿名内部类中可以有被 final 修饰的静态常量。
匿名类的典型使用场景:
- 临时使用:我们有时候需要添加一些类的临时实现去修复一些问题或者添加一些功能。为了避免在项目里添加java文件,尤其是仅使用一次这个类的时候,我们就会使用匿名类。
- UI Event Listeners:在java的图形界面编程中,匿名类最常使用的场景就是去创建一个事件监听器。比如:
button.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
}
});
上面的代码中,我们通过匿名类实现了 setOnClickListener 接口,当用户点击按钮的时候,就会触发我们实现的 onClick 方法。
多线程
Java 中的多线程就是利用多个线程共同完成一个大任务的运行过程,使用多线程可以最大程度的利用CPU。
使用多线程的使用线程而不是进程来做任务处理,是因为线程比进程更加轻量,线程是一个轻量级的进程,是程序执行的最小单元,并且线程和线程之间是共享主内存的,而进程不是。
线程生命周期
正如上图所示,线程生命周期一共有六种状态。我们现在依次对这些状态进行介绍。
- New:当我们构造出一个线程实例的时候, 这个线程就拥有了 New 状态。这个状态是线程的第一个状态。此时,线程并没有准备运行。
- Runnable:当调用了线程类的 start() 方法, 那么这个线程就会从 New 状态转换到 Runnable 状态。这就意味着这个线程要准备运行了。但是,如果线程真的要运行起来,就需要线程调度器来调度执行这个线程。但是线程调度器可能忙于在执行其他的线程,从而不能及时去调度执行这个线程。线程调度器是基于 FIFO 策略去从线程池中挑出一个线程来执行的。
- Blocked:线程可能会因为不同的情况自动的转为 Blocked 状态。比如,等候 I/O 操作,等候网络连接等等。除此之外,任意的优先级比当前正在运行的线程高的线程都可能会使得正在运行的线程转为 Blocked 状态。
- Waiting:在同步块中调用被同步对象的 wait 方法,当前线程就会进入 Waiting 状态。如果在另一个线程中的同一个对象被同步的同步块中调用 notify()/notifyAll(),就可能使得在 Waiting 的线程转入 Runnable 状态。
- Timed_Waiting:同 Waiting 状态,只是会有个时间限制,当超时了,线程会自动进入 Runnable 状态。
- Terminated:线程在线程的 run() 方法执行完毕后或者异常退出run()方法后,就会进入 Terminated 状态。
为什么要使用多线程
大白话讲就是通过多线程同时做多件事情让 Java 应用程序跑的更快,使用线程来实行并行和并发。如今的 CPU 都是多核并且频率很高,如果单独一个线程,并没有充分利用多核 CPU 的优势。
重要的优势
- 可以更好地利用 CPU
- 可以更好地提升和响应性相关的用户体验
- 可以减少响应时间
- 可以同时服务多个客户端
创建线程有两种方式
- 通过继承Thread类创建线程
这个继承类会重写 Thread 类的 run() 方法。一个线程的真正运行是从 run() 方法内部开始的,通过 start() 方法会去调用这个线程的 run() 方法。
public class MultithreadDemo extends Thread {
@Override
public void run() {
try {
System.out.println("线程 " + Thread.currentThread().getName() + " 现在正在运行");
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
MultithreadDemo multithreadDemo = new MultithreadDemo();
multithreadDemo.start();
}
}
}
- 通过实现Runnable接口创建线程
我们创建一个实现了 java.lang.Runnable 接口的新类,并实现其 run() 方法。然后我们会实例化一个 Thread 对象,并调用这个对象的 start() 方法。
public class MultithreadDemo implements Runnable {
@Override
public void run() {
try {
System.out.println("线程 " + Thread.currentThread().getName() + " 现在正在运行");
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(new MultithreadDemo());
thread.start();
}
}
}
两种创建方式对比
- 如果一个类继承了 Thread 类,那么这个类就没办法继承别的任何类了。因为 Java 是单继承,不允许同时继承多个类。多继承只能采用接口的方式,一个类可以实现多个接口。所以,使用实现 Runnable 接口在实践中比继承 Thread 类更好一些。
- 第一种创建方式,可以重写 yield()、interrupt() 等一些可能不太常用的方法。但是如果我们使用第二种方式去创建线程,则 yield() 等方法就无法重写了。
同步
同步只有在多线程条件下才有意义,一次只能有一个线程执行同步块。
在 Java 中,同步这个概念非常重要,因为 Java 本身就是一门多线程语言,在多线程环境中,做合适的同步是极度重要的。
为什么要使用同步
在多线程环境中执行代码,如果一个对象可以被多个线程访问,为了避免对象状态或者程序执行出现错误,对这个对象使用同步是非常必要的。
在深入讲解同步概念之前,我们先来看看同步相关的问题。
class Production {
//没有做方法同步
void printProduction(int n) {
for (int i = 1; i <= 5; i++) {
System.out.print(n * i+" ");
try {
Thread.sleep(400);
} catch (Exception e) {
System.out.println(e);
}
}
}
}
class MyThread1 extends Thread {
Production p;
MyThread1(Production p) {
this.p = p;
}
public void run() {
p.printProduction(5);
}
}
class MyThread2 extends Thread {
Production p;
MyThread2(Production p) {
this.p = p;
}
public void run() {
p.printProduction(100);
}
}
public class SynchronizationTest {
public static void main(String args[]) {
Production obj = new Production(); //多线程共享同一个对象
MyThread1 t1 = new MyThread1(obj);
MyThread2 t2 = new MyThread2(obj);
t1.start();
t2.start();
}
}
运行上面的代码后,由于我们没有加同步,可以看到运行结果非常混乱。
Output:
100 5 10 200 15 300 20 400 25 500
接下来,我们给 printProduction 方法加上同步:
class Production {
//做了方法同步
synchronized void printProduction(int n) {
for (int i = 1; i <= 5; i++) {
System.out.print(n * i+" ");
try {
Thread.sleep(400);
} catch (Exception e) {
System.out.println(e);
}
}
}
}
当我们对 printProduction() 加上了同步(synchronized)后, 已有一个线程执行的情况下,是不会有任何一个线程可以再次执行这个方法。这次加了同步后的输出结果是有次序的。
Output:
5 10 15 20 25 100 200 300 400 500
类似于对方法做同步,你也可以去同步 Java 类和对象。
注意:其实有时候我们可以不必去同步整个方法。出于性能原因,我们其实可以仅同步方法中我们需要同步的部分代码。被同步的这部分代码就是方法中的同步块。
序列化
Java 的序列化就是将一个 Java 对象转化为一个字节流的一种机制。从字节流再转回 Java 对象叫做反序列化,是序列化的反向操作。
序列化和反序列化是和平台无关的,也就是说你可以在 Linux 系统序列化,然后在 Windows 操作系统做反序列化。
如果要序列化对象,需要使用 ObjectOutputStream 类的 writeObject() 方法。如果要做反序列化,则要使用 ObjectOutputStream 类的 readObject() 方法。
如下图所示,对象被转化为字节流后,被储存在了不同的介质中。这个流程就是序列化。在图的右边,也可以看到从不同的介质中,比如内存,获得字节流并转化为对象,这叫做反序列化。
为什么使用序列化
如果我们创建了一个 Java 对象,这个对象的状态在程序执行完毕或者退出后就消失了,不会得到保存。
所以,为了能解决这类问题,Java 提供了序列化机制。这样,我们就能把对象的状态做临时储存或者进行持久化,以供后续当我们需要这个对象时,可以通过反序列化把对象还原回来。
下面给出一些代码看看我们是怎么来做序列化的。
import java.io.Serializable;
public class Player implements Serializable {
private static final long serialVersionUID = 1L;
private String serializeValueName;
private transient String nonSerializeValuePos;
public String getSerializeValueName() {
return serializeValueName;
}
public void setSerializeValueName(String serializeValueName) {
this.serializeValueName = serializeValueName;
}
public String getNonSerializeValueSalary() {
return nonSerializeValuePos;
}
public void setNonSerializeValuePos(String nonSerializeValuePos) {
this.nonSerializeValuePos = nonSerializeValuePos;
}
@Override
public String toString() {
return "Player [serializeValueName=" + serializeValueName + "]";
}
}
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
public class SerializingObject {
public static void main(String[] args) {
Player playerOutput = null;
FileOutputStream fos = null;
ObjectOutputStream oos = null;
playerOutput = new Player();
playerOutput.setSerializeValueName("niubi");
playerOutput.setNonSerializeValuePos("x:1000,y:1000");
try {
fos = new FileOutputStream("Player.ser");
oos = new ObjectOutputStream(fos);
oos.writeObject(playerOutput);
System.out.println("序列化数据被存放至Player.ser文件");
oos.close();
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Output:
序列化数据被存放至Player.ser文件
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
public class DeSerializingObject {
public static void main(String[] args) {
Player playerInput = null;
FileInputStream fis = null;
ObjectInputStream ois = null;
try {
fis = new FileInputStream("Player.ser");
ois = new ObjectInputStream(fis);
playerInput = (Player) ois.readObject();
System.out.println("从Player.ser文件中恢复");
ois.close();
fis.close();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println("player名字为 : " + playerInput.getSerializeValueName());
System.out.println("player位置为 : " + playerInput.getNonSerializeValuePos());
}
}
Output:
从Player.ser文件中恢复
player名字为 : niubi
player位置为 : null
关键特性
- 如果父类实现了 Serializable 接口那么子类就不必再实现 Serializable 接口了。但是反过来不行。
- 序列化只支持非 static 的成员变量
- static 修饰的变量和常量以及被 transient 修饰的变量是不会被序列化的。所以,如果我们不想要序列化某些非 static 的成员变量,直接用 transient 修饰它们就好了。
- 当反序列化对象的时候,是不会调用对象的构造函数的。
- 如果一个对象被一个要序列化的对象引用了,这个对象也会被序列化,并且这个对象也必须要实现 Serializable 接口。
总结
首先,我们介绍了匿名类的定义,使用场景和使用方式。
其次,我们讨论了多线程和其生命周期以及多线程的使用场景。
再次,我们了解了同步,知道同步后,仅同时允许一个线程执行被同步的方法或者代码块。当一个线程在执行被同步的代码时,别的线程只能在队列中等待直到执行同步代码的线程释放资源。
最后,我们知道了序列化就是把对象状态储存起来以供后续使用。
最最后,我准备了一些纯手打的高质量PDF,有好友赞助的也有我自己的,大家可以免费领取:
深入浅出Java多线程、HTTP超全汇总、Java基础核心总结、程序员必知的硬核知识大全、简历面试谈薪的超全干货。