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. Alink漫谈(二十) :卡方检验源码解析

    Alink漫谈(二十) :卡方检验源码解析 目录 Alink漫谈(二十) :卡方检验源码解析 0x00 摘要 0x01 背景概念 1.1 假设检验 1.2 H0和H1是什么? 1.3 P值 (P-va ...

  2. 焦大翻译:提防一些seo错误认知(完整版)

    http://www.wocaoseo.com/thread-179-1-1.html 多人在开始做seo的时候,都曾经尝试通过黑盒测试来找出哪些因素对排名有效果. 黑盒测试是我们IT行业常用术语,它 ...

  3. 2020,最新Model的设计-APP重构之路

    很多的app使用MVC设计模式来将“用户交互”与“数据和逻辑”分开,而model其中一个重要作用就是持久化.下文中设计的Model可能不是一个完美的,扩展性强的model范例,但在我需要重构的app中 ...

  4. 阿里大牛教你基于Python的 Selenium自动化测试示例解析

    今天给大家讲解的是自动化测试示例的解析,如有不对的地方请多多指教. 自动化测试示例如下: from selenium import webdriver from selenium.webdriver. ...

  5. el-dialog“闪动”解决办法

    问题描述:el-dialog关闭的时候总是出现两次弹窗 解决思路:既然是el-dialog产生的那就直接杀掉el-dialog 代码实践:在el-dialog上添加上一个v-if,值就是用闭窗的值,促 ...

  6. 一个神奇的jq插件----zTree

    最近在公司做项目中用到了一个树(ztree)的插件,使用起来非常顺手,便写下这篇博客,用来记录一下,以便后续使用 首先先放上ztree官方的地址:http://www.treejs.cn/v3/mai ...

  7. googleEarth

    中国航天科技集团有限公司 http://www.spacechina.com/n25/n144/n210/index.html Celestia [官网]: https://celestia.spac ...

  8. Toast 响应点击事件

    import java.lang.reflect.Field; import android.content.Context; import android.util.Log; import andr ...

  9. 【学习中】Unity插件之NGUI 完整视频教程

    课程 章节 内容 签到 Unity插件之NGUI 完整视频教程 第一章 NGUI基础控件和基础功能学习 1.NGUI介绍和插件的导入 6月29日 2.创建UIRoot 6月29日 3.学习Label控 ...

  10. mysql必知必会——GROUP BY和HAVING

    mysql必知必会——GROUP BY和HAVING 创建表结构 create table `employ_info` ( `id` int(11) NOT NULL AUTO_INCREMENT, ...