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() {
@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();
}
}
  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 {
@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();
}
}
}
  1. 通过实现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

关键特性

  1. 如果父类实现了 Serializable 接口那么子类就不必再实现 Serializable 接口了。但是反过来不行。
  2. 序列化只支持非 static 的成员变量
  3. static 修饰的变量和常量以及被 transient 修饰的变量是不会被序列化的。所以,如果我们不想要序列化某些非 static 的成员变量,直接用 transient 修饰它们就好了。
  4. 当反序列化对象的时候,是不会调用对象的构造函数的。
  5. 如果一个对象被一个要序列化的对象引用了,这个对象也会被序列化,并且这个对象也必须要实现 Serializable 接口。

总结

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

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

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

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

最最后,我准备了一些纯手打的高质量PDF,有好友赞助的也有我自己的,大家可以免费领取:

深入浅出Java多线程、HTTP超全汇总、Java基础核心总结、程序员必知的硬核知识大全、简历面试谈薪的超全干货。

别看数量不多,但篇篇都是干货,看完的都说很肝。

领取方式:扫码关注后,在公众号后台回复:666

听说这四个概念,很多 Java 老手都说不清的更多相关文章

  1. java各种概念 Core Java总结

    Base: OOA是什么?OOD是什么?OOP是什么?{ oo(object-oriented):基于对象概念,以对象为中心,以类和继承为构造机制,来认识,理解,刻画客观世界和设计,构建相应的软件系统 ...

  2. J2EE进阶(十四)超详细的Java后台开发面试题之Spring IOC与AOP

    J2EE进阶(十四)超详细的Java后台开发面试题之Spring IOC与AOP 前言   搜狐畅游笔试题中有一道问答题涉及到回答谈谈对Spring IOC与AOP的理解.特将相关内容进行整理.    ...

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

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

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

    目录 一.写在前面 二.技术广度的快速准备 三.技术深度的快速准备 四.基础功底的快速准备 五.下篇预告 一.写在前面 春节长假转眼已过,即将迎来的是一年一度的金三银四跳槽季. 假如你准备在金三银四跳 ...

  5. JVM 内部原理(五)— 基本概念之 Java 虚拟机官方规范文档,第 7 版

    JVM 内部原理(五)- 基本概念之 Java 虚拟机官方规范文档,第 7 版 介绍 版本:Java SE 7 每位使用 Java 的程序员都知道 Java 字节码在 Java 运行时(JRE - J ...

  6. 两个变量交换的四种方法(Java) 七种方法(JS)

    两个变量交换的四种方法(Java)   对于两种变量的交换,我发现四种方法,下面我用Java来演示一下. 1.利用第三个变量交换数值,简单的方法. (代码演示一下) 1 class TestEV 2 ...

  7. java反射(四)--反射与简单java类

    一.传统简单java类 简单的java类主要是由属性所组成,并且提供有相应的setter以及getter的处理方法,同时简单java类最大的特征就是通过对象保存相应的类的属性内容,但是如果使用传统的简 ...

  8. java向MySQL插入当前时间的四种方式和java时间日期格式化的几种方法(案例说明)

    转载地址:http://www.devba.com/index.php/archives/4581.html java向MySQL插入当前时间的四种方式和java时间日期格式化的几种方法(案例说明); ...

  9. (转)java向MySQL插入当前时间的四种方式和java时间日期格式化的几种方法(案例说明)

    java向MySQL插入当前时间的四种方式和java时间日期格式化的几种方法(案例说明);部分资料参考网络资源 1. java向MySQL插入当前时间的四种方式 第一种:将java.util.Date ...

随机推荐

  1. 完美解决方案-雪花算法ID到前端之后精度丢失问题

    最近公司的一个项目组要把以前的单体应用进行为服务拆分,表的ID主键使用Mybatis plus默认 的雪花算法来生成. 快下班的时候,小伙伴跑过来找我,:"快给我看看这问题,卡这卡了小半天了 ...

  2. [PyTorch 学习笔记] 2.1 DataLoader 与 DataSet

    thumbnail: https://image.zhangxiann.com/jeison-higuita-W19AQY42rUk-unsplash.jpg toc: true date: 2020 ...

  3. vue前端工程化

    今日目标 1.能够了解模块化的相关规范 2.了解webpack3.了解使用Vue单文件组件4.能够搭建Vue脚手架 5.掌握Element-UI的使用 1.模块化的分类 A.浏览器端的模块化   1) ...

  4. xmind8 破解激活教程

    这里以windows为例来演示,其它操作系统需根据情况修改相应步骤. 一.下载安装包 首先去xmind国外官网下载对应操作系统的安装包,国内官网的那个是有残缺的,不支持破解. 官网下载链接 二.下载破 ...

  5. Kafka入门(3):Sarama生产者是如何工作的

    摘要 在这一篇的文章中,我将从Sarama的同步生产者和异步生产者怎么创建开始讲起,然后我将向你介绍生产者中的各个参数是什么,怎么使用. 然后我将从创建生产者的代码开始,按照代码的调用流程慢慢深入,直 ...

  6. js实现csv下载

    var TableDatas  = '数据源';function getCSV(){ if(this.TableDatas.length === 0){ alert("没有数据呀呀呀呀!&q ...

  7. 初学WebGL引擎-BabylonJS:第3篇-方向纹理与相机

    [playground]-rotatuib abd scaling(方向) 源码 var createScene = function () { var scene = new BABYLON.Sce ...

  8. selenium上手

    功能自动化 前提 自动化的主要目的并不是为了找Bug,是为了证明功能可用 不只是所有的功能都可以自动化,如UI 并不是所有的项目都可以使用自动化,如selenium只能使用bs项目,小项目不适合使用自 ...

  9. 1090 Highest Price in Supply Chain (25 分)(模拟建树,找树的深度)牛客网过,pat没过

    A supply chain is a network of retailers(零售商), distributors(经销商), and suppliers(供应商)-- everyone invo ...

  10. 跨年夜问题:一句并不复杂的delete竟然在delete statement处cost飙升,在数据量上升的十万级就像进入了死循环,执行后久久没有结果

    笔者使用的环境: # 类别 版本 1 操作系统 Win10 2 数据库 Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - 64bi ...