Java中难理解的四个概念
前⾔
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。
使⽤多线程的使⽤线程⽽不是进程来做任务处理,是因为线程⽐进程更加轻量,线程是⼀个轻量级的进程,是程序执⾏的最⼩单元,并且线程和线程之间是共享主内存的,⽽进程不是。
线程⽣命周期
正如上图所⽰,线程⽣命周期⼀共有六种状态。我们现在依次对这些状态进⾏介绍。
1. New:当我们构造出⼀个线程实例的时候, 这个线程就拥有了 New 状态。这个状态是线程的第⼀个状态。此时,线程并没有准备运⾏。
2. Runnable:当调⽤了线程类的 start() ⽅法, 那么这个线程就会从 New 状态转换到 Runnable 状态。
这就意味着这个线程要准备运⾏
了。但是,如果线程真的要运⾏起来,就需要线程调度器来调度执⾏这个线程。但是线程调度器可能忙于在执⾏其他的线程,从⽽不能及时去调度执⾏这个线程。线程调度器是基于 FIFO 策略去从线程池中挑出⼀个线程来执⾏的。
3. Blocked:线程可能会因为不同的情况⾃动的转为 Blocked 状态。⽐如,等候 I/O 操作,等候⽹络连接等等。除此之外,任意的优先
级⽐当前正在运⾏的线程⾼的线程都可能会使得正在运⾏的线程转为 Blocked 状态。
4. Waiting:在同步块中调⽤被同步对象的 wait ⽅法,当前线程就会进⼊ Waiting 状态。如果在另⼀个线程中的同⼀个对象被同步的同
步块中调⽤ notify()/notifyAll(),就可能使得在 Waiting 的线程转⼊ Runnable 状态。
5. Timed_Waiting:同 Waiting 状态,只是会有个时间限制,当超时了,线程会⾃动进⼊ Runnable 状态。
6. Terminated:线程在线程的 run() ⽅法执⾏完毕后或者异常退出run()⽅法后,就会进⼊ Terminated 状态。
为什么要使⽤多线程
⼤⽩话讲就是通过多线程同时做多件事情让 Java 应⽤程序跑的更快,使⽤线程来实⾏并⾏和并发。如今的 CPU 都是多核并且频率很⾼,如果单独⼀个线程,并没有充分利⽤多核 CPU 的优势。
重要的优势
可以更好地利⽤ CPU
可以更好地提升和响应性相关的⽤户体验
可以减少响应时间
java类的概念可以同时服务多个客户端
创建线程有两种⽅式
1.通过继承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();
}
}
}
2.通过实现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();
}
}