Java 是很多人一直在用的编程语言,但是有些 Java 概念是非常难以理解的,哪怕是一些多年的老手,对某些 Java 概念也存在一些混淆和困惑。

所以,在这篇文章里,会介绍四个 Java 中最难理解的四个概念,去帮助开发者更清晰的理解这些概念:

  1. 匿名内部类的用法

  2. 多线程

  3. 如何实现同步

  4. 序列化

匿名内部类

匿名内部类又叫匿名类,它有点像局部类(Local Class)或者内部类(Inner Class),只是匿名内部类没有名字,我们可以同时声明并实例化一个匿名内部类。

一个匿名内部类仅适用在想使用一个局部类并且只会使用这个局部类一次的场景。

匿名内部类是没有需要明确声明的构造函数的,但是会有一个隐藏的自动声明的构造函数。

创建匿名内部类有两种办法:

  1. 通过继承一个类(具体或者抽象都可以)去创建出匿名内部类

  2. 通过实现一个接口创建出匿名内部类

咱们看看下面的例子:

interface Programmer {void develop();
}public class TestAnonymousClass {public static Programmer programmer = new Programmer() {@Overridepublic void develop() {System.out.println("我是在类中实现了接口的匿名内部类");}};public static void main(String[] args) {Programmer anotherProgrammer = new Programmer() {@Overridepublic 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) {@Overridepublic void showAge() {System.out.println("在匿名类中的showAge方法:" + age);}};programmer.showAge();}
}
  1. 构造匿名类时,我们可以使用任何构造函数。上面的代码可以看到我们使用了带参数的构造函数。

  2. 匿名类可以继承具体类或者抽象类,也能实现接口。所以访问修饰符规则同普通类是一样的。子类可以访问父类中的 protected 限制的属性,但是无法访问 private 限制的属性。

  3. 如果匿名类继承了具体类,比如上面代码中的 Programmer 类,那么就不必重写所有方法。但是如果匿名类继承了一个抽象类或者实现了一个接口,那么这个匿名类就必须实现所有没有实现的抽象方法。

  4. 在一个匿名内部类中你不能使用静态初始化,也没办法添加静态变量。

  5. 匿名内部类中可以有被 final 修饰的静态常量。

匿名类的典型使用场景

  1. 临时使用:我们有时候需要添加一些类的临时实现去修复一些问题或者添加一些功能。为了避免在项目里添加java文件,尤其是仅使用一次这个类的时候,我们就会使用匿名类。

  2. 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

  • 可以更好地提升和响应性相关的用户体验

  • 可以减少响应时间

  • 可以同时服务多个客户端

创建线程有两种方式

  1. 通过继承Thread类创建线程

这个继承类会重写 Thread 类的 run() 方法。一个线程的真正运行是从 run() 方法内部开始的,通过 start() 方法会去调用这个线程的 run() 方法。

public class MultithreadDemo extends Thread {@Overridepublic 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();}}
}
  1. 通过实现Runnable接口创建线程

我们创建一个实现了 java.lang.Runnable 接口的新类,并实现其 run() 方法。然后我们会实例化一个 Thread 对象,并调用这个对象的 start() 方法。

public class MultithreadDemo implements Runnable {@Overridepublic 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;}@Overridepublic 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

关键特性

  1. 如果父类实现了 Serializable 接口那么子类就不必再实现 Serializable 接口了。但是反过来不行。

  2. 序列化只支持非 static 的成员变量

  3. static 修饰的变量和常量以及被 transient 修饰的变量是不会被序列化的。所以,如果我们不想要序列化某些非 static 的成员变量,直接用 transient 修饰它们就好了。

  4. 当反序列化对象的时候,是不会调用对象的构造函数的。

  5. 如果一个对象被一个要序列化的对象引用了,这个对象也会被序列化,并且这个对象也必须要实现 Serializable 接口。

总结

首先,我们介绍了匿名类的定义,使用场景和使用方式。

其次,我们讨论了多线程和其生命周期以及多线程的使用场景。

再次,我们了解了同步,知道同步后,仅同时允许一个线程执行被同步的方法或者代码块。当一个线程在执行被同步的代码时,别的线程只能在队列中等待直到执行同步代码的线程释放资源。

最后,我们知道了序列化就是把对象状态储存起来以供后续使用。

特别推荐一个分享架构+算法的优质内容,还没关注的小伙伴,可以长按关注一下:长按订阅更多精彩▼如有收获,点个在看,诚挚感谢

什么?听说这四个概念,很多 Java 老手都说不清!相关推荐

  1. java 网络编程connection timed out是什么意思_什么?听说这四个概念,很多 Java 老手都说不清...

    Java 是很多人一直在用的编程语言,但是有些 Java 概念是非常难以理解的,哪怕是一些多年的老手,对某些 Java 概念也存在一些混淆和困惑. 所以,在这篇文章里,会介绍四个 Java 中最难理解 ...

  2. 什么?听说很多Java老司机都栽在了这四个概念上?

    Java 是很多人一直在用的编程语言,但是有些 Java 概念是非常难以理解的,哪怕是一些多年的老手,对某些 Java 概念也存在一些混淆和困惑.所以,在这篇文章里,会介绍四个 Java 中最难理解的 ...

  3. 异常的概念和Java异常体系结构

    一. 异常的概念和Java异常体系结构 异常是程序运行过程中出现的错误.本文主要讲授的是Java语言的异常处理.Java语言的异常处理框架,     是Java语言健壮性的一个重要体现. Java把异 ...

  4. 【建议收藏】2020年中高级Android大厂面试秘籍,为你保驾护航金三银四,直通大厂(Java篇)

    Java面试题 Java基础 一.面向对象 (⭐⭐⭐) 1.谈谈对java多态的理解? 多态是指父类的某个方法被子类重写时,可以产生自己的功能行为,同一个操作作用于不同对象,可以有不同的解释,产生不同 ...

  5. 【建议收藏】2021年中高级Android大厂面试秘籍,为你保驾护航金三银四,直通大厂(Java篇)

    前言 成为一名优秀的Android开发,需要一份完备的知识体系,在这里,让我们一起成长为自己所想的那样~. A awesome android expert interview questions a ...

  6. 【金三银四跳槽季】Java工程师如何在1个月内做好面试准备?

    点击上方"蓝字", 右上角选择"设为星标" 周一至周五早11点半!精品文章准时送上! 本文来自石杉的架构笔记 目录 一.写在前面 二.技术广度的快速准备 三.技 ...

  7. java编译会产生多少个类文件,编译一个定义了三个类和四个方法的Java源程序文件,总共会产生多少个字节码文件 ? ( )...

    编译一个定义了三个类和四个方法的Java源程序文件,总共会产生多少个字节码文件 ? ( ) 更多相关问题 论述风化作用基本概念及其主要类型. 什么是药用植物 学?其 研究任务是什么 ? 庐山瀑布很有名 ...

  8. JAVA基础再回首(一)——基本概念、JAVA开发工具、JAVA基本语法

    JAVA基础再回首(一)--基本概念.JAVA开发工具.JAVA基本语法 学了java,做了android应用开发,现在回想起来,真的是一路坎坷..我在上章博客中提到了我以后的几个学习计划和目标就是把 ...

  9. Web应用的概念以及Java Web应用的发展路程

    简单介绍了Web应用的概念以及Java Web应用的发展路程. 文章目录 1 动态Web 应用 2 早期的CGI 3 Java的Servlet 4 更进一步的JSP 5 兼容并取的MVC 1 动态We ...

最新文章

  1. 交叉编译 FLTK1.3.0
  2. 3.7 为什么需要非线性激活函数-深度学习-Stanford吴恩达教授
  3. 二叉树先序遍历递归算法(图解)
  4. QT的QSortFilterProxyModel类的使用
  5. 中求和符号上下标_涨电脑知识:如何在word中编写复杂的公式,写论文必备技能...
  6. 对象的多数组表示(不一样的链表-多数组表示链表)
  7. oracle today函数,oracle日期函数集锦
  8. jquery中怎么删除ul中的整个li包括节点
  9. 关于git远程版本库的一些问题之解决
  10. xp系统mysql安装教程视频教程_Windows XP操作系统下的MYSQL安装过程_PHP教程
  11. json 插入数据_MongoDB如何一次插入多条json数据
  12. centos 7 之nginx
  13. npm install -g @vue/cli时 -4048 npm ERR! Error: EPERM: operation not permitted, lstat报错的几种解决方案
  14. 如何进入ThinkSystem系列服务器SAS RAID阵列配置界面
  15. 怎么隐藏电脑桌面计算机,电脑怎么隐藏桌面图标?
  16. 数据结构-单链表LNode,*LinkList
  17. 对话马丁·福勒(Martin Fowler)——第六部分:性能与过程调优
  18. 数据库---学生选课查询案例---经典查询题
  19. C PRIMER PLUS第七章11题
  20. Linux 命令ps aux命令解析

热门文章

  1. linux shell unlink,linux shell中,unlink和rm命令有什么区别
  2. eclipse提示jre_给新手 Java 开发者的 7 点提示 | Linux 中国
  3. 使用C++的Socket实现从客户端到服务端,服务端到客户端传输文件
  4. poj2553(强连通分量)
  5. 最小割 ---- 集合冲突模型 ----- P1646 [国家集训队]happiness
  6. P3366 【模板】最小生成树(链式前向星,prim,有坑)难度⭐⭐
  7. 解表化饮什么意思_为什么有人动不动就一身汗,有人再热也不出汗?中医告诉真实原因...
  8. php的正则怎么写,一个正则的写法 php
  9. 后处理程序文件大小的变量_【每日一题】(17题)面试官问:JS中事件流,事件处理程序,事件对象的理解?...
  10. 井下关于风速的规定_【市企融合】金川集团井下“5G+矿运卡车”远程遥控系统科研项目实验成功...