相关推荐阅读:
Java中常用的设计模式(附代码实现和具体的应用场景)
Java面试题目录
-
- Java概述
- Java基础
-
- 8 Java有哪些数据类型
- 9 switch 是否能作用在byte上,是否能作用在long上,是否能作用在String上
- 10 用最有效率的方法计算2乘以8
- 11 Math.round(11.5) 等于多少?Math.round(-11.5)等于多少
- 12 float f=3.4;是否正确
- 13 short s1 = 1; s1 = s1 + 1;有错吗?short s1 = 1; s1 += 1;有错吗
- 14 Java语言采用何种编码方案?有何特点?
- 15 什么Java注释
- 16 访问修饰符 public,private,protected,以及不写(默认)时的区别
- 17 &和&&的区别
- 18 Java 有没有 goto
- 19 final 有什么用?
- 20 final finally finalize区别
- 21 this关键字的用法
- 22 super关键字的用法
- 23 this与super的区别
- 24 static存在的主要意义
- 25 static的独特之处
- 26 static应用场景
- 27 static注意事项
- 28 break ,continue ,return 的区别及作用
- 29 在Java中,如何跳出当前的多重嵌套循环
- 面向对象
- 类与接口
- 变量与方法
- 重写与重载
- 对象相等判断
- 值传递
- IO流
- 反射
- String相关
-
- 61 字符型常量和字符串常量的区别?
- 63 什么是字符串常量池?
- 63 String 是最基本的数据类型吗?
- 64 String有哪些特性?
- 65 String为什么是不可变的吗?
- 66 是否可以继承 String 类?
- 67 String str="i"与 String str=new String(“i”)一样吗?
- 68 String s = new String(“xyz”);创建了几个字符串对象?
- 69 String 类的常用方法都有那些?
- 70 在使用 HashMap 的时候,用 String 做 key 有什么好处?
- 71 String和StringBuffer、StringBuilder的区别是什么?String为什么是不可变的?
- 包装类相关
- 集合容器概述
- List集合
- Set集合
- Map集合
-
- 94 HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底层实现
- 95 HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底层实现
- 96 HashMap的put方法的具体流程
- 97 HashMap的扩容操作是怎么实现的?
- 98 HashMap默认加载因子为什么选择0.75?
- 99 为什么要将链表中转红黑树的阈值设为8?为什么不一开始直接使用红黑树?
- 100 HashMap是怎么解决哈希冲突的?
- 101 HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?
- 102 能否使用任何类作为 Map 的 key?
- 103 为什么HashMap中String、Integer这样的包装类适合作为Key?
- 104 HashMap 多线程导致死循环问题
- 105 ConcurrentHashMap 底层具体实现
- 106 HashTable的底层实现
- 107 HashMap、ConcurrentHashMap及Hashtable 的区别
- Java集合的常用方法
- Java异常概述
- Java异常常见面试题
- 多线程和并发编程
-
- 126 线程和进程的区别是什么?
- 127 线程死锁是如何产生的,如何避免?
- 128 Java 实现线程有哪几种方式?
- 129 runnable 和 callable 有什么区别?
- 130 线程的run()和start()有什么区别?
- 131 为什么调用start()方法时会执行run()方法,而不直接执行run()方法?
- 132一个线程的生命周期有哪几种状态?它们之间如何流转的?
- 133 线程同步以及线程调度相关的方法有哪些?
- 134 sleep()方法和wait()方法的区别?
- 135 线程的sleep()方法和yield()方法有什么不同?
- 136 线程通信的方法有哪些?
- 137什么是线程同步?什么是线程互斥?他们是如何实现的?
- 138 三个线程T1、T2、T3,如何让他们按顺序执行?
- 139 为什么要使用线程池?
- 多线程其他知识补充
Java概述
1 何为编程
- 编程就是让计算机为解决某个问题而使用某种程序设计语言编写程序代码,并最终得到结果的过程。
- 为了使计算机能够理解人的意图,人类就必须要将需解决的问题的思路、方法、和手段通过计算机能够理解的形式告诉计算机,使得计算机能够根据人的指令一步一步去工作,完成某种特定的任务。这种人和计算机之间交流的过程就是编程。
2 什么是Java
- Java是一门面向对象编程语言,不仅吸收了C++语言的各种优点,还摒弃了C++里难以理解的多继承、指针等概念,因此Java语言具有功能强大和简单易用两个特征。Java语言作为静态面向对象编程语言的代表,极好地实现了面向对象理论,允许程序员以优雅的思维方式进行复杂的编程 。
3 jdk1.5之后的三大版本
- Java SE(J2SE,Java 2 Platform Standard Edition,标准版)
Java SE 以前称为 J2SE。它允许开发和部署在桌面、服务器、嵌入式环境和实时环境中使用的 Java应用程序。Java SE 包含了支持 Java Web 服务开发的类,并为Java EE和Java ME提供基础。 - Java EE(J2EE,Java 2 Platform Enterprise Edition,企业版)
Java EE 以前称为 J2EE。企业版本帮助开发和部署可移植、健壮、可伸缩且安全的服务器端Java 应用程序。Java EE 是在 Java SE 的基础上构建的,它提供 Web 服务、组件模型、管理和通信 API,可以用来实现企业级的面向服务体系结构(service-oriented architecture,SOA)和 Web2.0应用程序。2018年2月,Eclipse 宣布正式将 JavaEE 更名为 JakartaEE - Java ME(J2ME,Java 2 Platform Micro Edition,微型版)
Java ME 以前称为 J2ME。Java ME 为在移动设备和嵌入式设备(比如手机、PDA、电视机顶盒和打印机)上运行的应用程序提供一个健壮且灵活的环境。Java ME 包括灵活的用户界面、健壮的安全模型、许多内置的网络协议以及对可以动态下载的连网和离线应用程序的丰富支持。基于 JavaME 规范的应用程序只需编写一次,就可以用于许多设备,而且可以利用每个设备的本机功能。
4 Jdk和Jre和JVM的区别
- JDK :Jdk还包括了一些Jre之外的东西 ,就是这些东西帮我们编译Java代码的, 还有就是监控Jvm的一些工具 Java Development Kit是提供给Java开发人员使用的,其中包含了Java的开发工具,也包括了JRE。所以安装了JDK,就无需再单独安装JRE了。其中的开发工具:编译工具(javac.exe),打包工具(jar.exe)等。
- JRE :Jre大部分都是 C 和 C++ 语言编写的,他是我们在编译java时所需要的基础的类库 JavaRuntime Environment包括Java虚拟机和Java程序所需的核心类库等。核心类库主要是java.lang包:包含了运行Java程序必不可少的系统类,如基本数据类型、基本数学函数、字符串处理、线程、异常处理类等,系统缺省加载这个包。
如果想要运行一个开发好的Java程序,计算机中只需要安装JRE即可。
- Jvm:在倒数第二层 由他可以在(最后一层的)各种平台上运行 Java Virtual Machine是Java虚拟机,Java程序需要运行在虚拟机上,不同的平台有自己的虚拟机,因此Java语言可以实现跨平台。
5 什么是跨平台性?原理是什么
- 所谓跨平台性,是指java语言编写的程序,一次编译后,可以在多个系统平台上运行。
- 实现原理:Java程序是通过java虚拟机在系统平台上运行的,只要该系统可以安装相应的java虚拟机,该系统就可以运行java程序。
6 Java语言有哪些特点
- 简单易学(Java语言的语法与C语言和C++语言很接近)
- 面向对象(封装,继承,多态)
- 平台无关性(Java虚拟机实现平台无关性)
- 支持网络编程并且很方便(Java语言诞生本身就是为简化网络编程设计的)
- 平支持多线程(多线程机制使应用程序在同一时间并行执行多项任)
- 支持多线程(多线程机制使应用程序在同一时间并行执行多项任)
- 安全性好
7 什么是字节码?采用字节码的最大好处是什么
- 字节码:Java源代码经过虚拟机编译器编译后产生的文件(即扩展为.class的文件),它不面向任何特定的处理器,只面向虚拟机。
- 采用字节码的好处:Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行。
- 先看下java中的编译器和解释器:Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在Java中,这种供虚拟机理解的代码叫做字节码(即扩展为.class的文件),它不面向任何特定的处理器,只面向虚拟机。每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行,这就是上面提到的Java的特点的编译与解释并存的解释。Java源代码---->编译器---->jvm可执行的Java字节码(即虚拟指令)---->jvm---->jvm中解释器----->机器可执行的二进制机器码---->程序运行。
Java基础
8 Java有哪些数据类型
定义:Java语言是强类型语言,对于每一种数据都定义了明确的具体的数据类型,在内存中分配了不同大小的内存空间。
分类:
- 基本数据类型
- 数值型
- 整数类型(byte,short,int,long)
- 浮点类型(float,double)
- 字符型(char)
- 布尔型(boolean)
- 引用数据类型
- 类(class)
- 接口(interface)
- 数组([])
- 类(class)
Java基本数据类型图
9 switch 是否能作用在byte上,是否能作用在long上,是否能作用在String上
- 在Java 5以前,switch(expr)中,expr 只能是 byte、short、char、int。从 Java5 开始,Java 中引入了枚举类型,expr 也可以是 enum 类型,从 Java 7 开始,expr 还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的。
10 用最有效率的方法计算2乘以8
- 2 << 3(左移 3 位相当于乘以 2 的 3 次方,右移 3 位相当于除以 2 的 3 次方)。
11 Math.round(11.5) 等于多少?Math.round(-11.5)等于多少
- Math.round(11.5)的返回值是 12,Math.round(-11.5)的返回值是-11。四舍五入的原理是在参数上加 0.5 然后进行下取整。
12 float f=3.4;是否正确
- 不正确。3.4 是双精度数,将双精度型(double)赋值给浮点型(float)属于下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换float f =(float)3.4; 或者写成 floatf=3.4F。
13 short s1 = 1; s1 = s1 + 1;有错吗?short s1 = 1; s1 += 1;有错吗
- 对于 short s1 = 1; s1 = s1 + 1;由于 1 是 int 类型,因此 s1+1 运算结果也是 int型,需要强制转换类型才能赋值给 short 型。
- 而 short s1 = 1; s1 += 1;可以正确编译,因为 s1+= 1;相当于 s1 = (short(s1 + 1);其中有隐含的强制类型转换。
14 Java语言采用何种编码方案?有何特点?
- Java语言采用Unicode编码标准,Unicode(标准码),它为每个字符制订了一个唯一的数值,因此在任何的语言,平台,程序都可以放心的使用。
15 什么Java注释
定义:用于解释说明程序的文字
分类:
- 单行注释
格式: // 注释文字 - 多行注释
格式: /* 注释文字 */ - 文档注释
格式:/** 注释文字 */
作用:
- 在程序中,尤其是复杂的程序中,适当地加入注释可以增加程序的可读性,有利于程序的修改、调试和交流。注释的内容在程序编译的时候会被忽视,不会产生目标代码,注释的部分不会对程序的执行结果产生任何影响。
16 访问修饰符 public,private,protected,以及不写(默认)时的区别
- 定义:Java中,可以使用访问修饰符来保护对类、变量、方法和构造方法的访问。Java 支持 4 种不同的访问权限。
- 分类:
- private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)
- default (即缺省,什么也不写,不使用任何关键字): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。
- protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)。
- public : 对所有类可见。使用对象:类、接口、变量、方法
访问修饰符图
17 &和&&的区别
- &运算符有两种用法:(1)按位与;(2)逻辑与。
- &&运算符是短路与运算。逻辑与跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端的布尔值都是true 整个表达式的值才是 true。&&之所以称为短路运算,是因为如果&&左边的表达式的值是 false,右边的表达式会被直接短路掉,不会进行运算。
18 Java 有没有 goto
- goto 是 Java 中的保留字,在目前版本的 Java 中没有使用
19 final 有什么用?
用于修饰类、属性和方法:
- 被final修饰的类不可以被继承
- 被final修饰的方法不可以被重写
- 被final修饰的变量不可以被改变,被final修饰不可变的是变量的引用,而不是引用指向的内容,引用指向的内容是可以改变的
20 final finally finalize区别
- final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表 示该变量是一个常量不能被重新赋值。
- finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块 中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。
- finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调 用,当我们调用System.gc() 方法的时候,由垃圾回收器调用finalize(),回收垃圾,一个对象是否可回收的 最后判断。
21 this关键字的用法
-
this是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针。
-
this的用法在java中大体可以分为3种:
1.普通的直接引用,this相当于是指向当前对象本身。
2.形参与成员名字重名,用this来区分:public Person(String name, int age) { this.name = name; this.age = age; }
3.引用本类的构造函数
class Person{ private String name; private int age; public Person() { } public Person(String name) { this.name = name; } public Person(String name, int age) { this(name); this.age = age; } }
22 super关键字的用法
-
super可以理解为是指向自己超(父)类对象的一个指针,而这个超类指的是离自己最近的一个父类。
-
super也有三种用法:
1.普通的直接引用:
与this类似,super相当于是指向当前对象的父类的引用,这样就可以用super.xxx来引用父类的成员。
2.子类中的成员变量或方法与父类中的成员变量或方法同名时,用super进行区分:class Person{ protected String name; public Person(String name) { this.name = name; } } class Student extends Person{ private String name; public Student(String name, String name1) { super(name); this.name = name1; } public void getInfo(){ System.out.println(this.name); //Child System.out.println(super.name); //Father } } public class Test { public static void main(String[] args) { Student s1 = new Student("Father","Child"); s1.getInfo(); } }
3.引用父类构造函数:
- super(参数):调用父类中的某一个构造函数(应该为构造函数中的第一条语句)。
- this(参数):调用本类中另一种形式的构造函数(应该为构造函数中的第一条语句)。
23 this与super的区别
- super: 它引用当前对象的直接父类中的成员(用来访问直接父类中被隐藏的父类中成员数据或函数,基类与派生类中有相同成员定义时如:super.变量名 super.成员函数据名(实参)
- this:它代表当前对象名(在程序中易产生二义性之处,应使用this来指明当前对象;如果函数的形参与类中的成员数据同名,这时需用this来指明成员变量名)
- super()和this()类似,区别是,super()在子类中调用父类的构造方法,this()在本类内调用本类的其它构造方法。
- super()和this()均需放在构造方法内第一行。
- 尽管可以用this调用一个构造器,但却不能调用两个。
- this和super不能同时出现在一个构造函数里面,因为this必然会调用其它的构造函数,其它的构造函数必然也会有super语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。
- this()和super()都指的是对象,所以,均不可以在static环境中使用。包括:static变量,static方法,static语句块。
- 从本质上讲,this是一个指向本对象的指针, 然而super是一个Java关键字。
24 static存在的主要意义
- static的主要意义是在于创建独立于具体对象的域变量或者方法。以致于即使没有创建对象,也能使用属性和调用方法!
- static关键字还有一个比较关键的作用就是 用来形成静态代码块以优化程序性能。static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。
- 为什么说static块可以用来优化程序性能,是因为它的特性:只会在类加载的时候执行一次。因此,很多时候会将一些只需要进行一次的初始化操作都放在static代码块中进行。
25 static的独特之处
-
被static修饰的变量或者方法是独立于该类的任何对象,也就是说,这些变量和方法不属于任何一个实例对象,而是被类的实例对象所共享。
怎么理解 “被类的实例对象所共享” 这句话呢?就是说,一个类的静态成员,它是属于大伙的【大伙指的是这个类的多个对象实例,我们都知道一个类可以创建多个实例!】,所有的类对象共享的,不像成员变量是自个的【自个指的是这个类的单个实例对象】…我觉得我已经讲的很通俗了,你明白了咩?
-
在该类被第一次加载的时候,就会去加载被static修饰的部分,而且只在类第一次使用时加载并进行初始化,注意这是第一次用就要初始化,后面根据需要是可以再次赋值的。
-
static变量值在类加载的时候分配空间,以后创建类对象的时候不会重新分配。赋值的话,是可以任意赋值的!
-
被static修饰的变量或者方法是优先于对象存在的,也就是说当一个类加载完毕之后,即便没有创建对象,也可以去访问。
26 static应用场景
-
因为static是被类的实例对象所共享,因此如果某个成员变量是被所有对象所共享的,那么这个成员变量就应该定义为静态变量。
-
因此比较常见的static应用场景有:
1、修饰成员变量 2、修饰成员方法 3、静态代码块 4、修饰类【只能修饰内部类也就是静态内部类】 5、静态导包
27 static注意事项
- 静态只能访问静态。
- 非静态既可以访问非静态的,也可以访问静态的。
28 break ,continue ,return 的区别及作用
- break 跳出总上一层循环,不再执行循环(结束当前的循环体)
- continue 跳出本次循环,继续执行下次循环(结束正在执行的循环 进入下一个循环条件)
- return 程序返回,不再执行下面的代码(结束当前的方法 直接返回)
29 在Java中,如何跳出当前的多重嵌套循环
-
在Java中,要想跳出多重循环,可以在外面的循环语句前定义一个标号,然后在里层循环体的代码中使用带有标号的break 语句,即可跳出外层循环。例如:
public static void main(String[] args) { ok: for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; j++) { System.out.println("i=" + i + ",j=" + j); if (j == 5) { break ok; } } } }
面向对象
30 面向对象和面向过程的区别
面向过程:
- 优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
- 缺点:没有面向对象易维护、易复用、易扩展。
面向对象:
- 优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护。
- 缺点:性能比面向过程低
总结:
面向过程是具体化的,流程化的,解决一个问题,你需要一步一步的分析,一步一步的实现。
面向对象是模型化的,你只需抽象出一个类,这是一个封闭的盒子,在这里你拥有数据也拥有解决问题的方法。需要什么功能直接使用就可以了,不必去一步一步的实现,至于这个功能是如何实现的,管我们什么事?我们会用就可以了。
面向对象的底层其实还是面向过程,把面向过程抽象成类,然后封装,方便我们使用的就是面向对象了。
31 面向对象的特征有哪些方面
面向对象的特征主要有以下几个方面:
-
抽象:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。
-
封装:把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。
-
继承:是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。
关于继承如下 3 点请记住:
- 子类拥有父类非 private 的属性和方法。
- 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
- 子类可以用自己的方式实现父类的方法。
-
多态:父类或接口定义的引用变量可以指向子类或具体实现类的实例对象。提高了程序的拓展性。在Java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。
32 什么是多态机制?Java语言是如何实现多态的?
- 所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。
- 多态分为编译时多态和运行时多态。其中编辑时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的函数,通过编辑之后会变成两个不同的函数,在运行时谈不上多态。而运行时多态是动态的,它是通过动态绑定来实现的,也就是我们所说的多态性。
多态的实现
-
Java实现多态有三个必要条件:继承、重写、向上转型。
- 继承:在多态中必须存在有继承关系的子类和父类。
- 重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
- 向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。
只有满足了上述三个条件,我们才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而达到执行不同的行为。
对于Java而言,它多态的实现机制遵循一个原则:当超类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。
33 面向对象五大基本原则是什么(可选)
- 单一职责原则SRP(Single Responsibility Principle)
类的功能要单一,不能包罗万象,跟杂货铺似的。 - 开放封闭原则OCP(Open-Close Principle)
一个模块对于拓展是开放的,对于修改是封闭的,想要增加功能热烈欢迎,想要修改,哼,一万个不乐意。 - 里式替换原则LSP(the Liskov Substitution Principle LSP)子类可以替换父类出现在父类能够出现的任何地方。
- 依赖倒置原则DIP(the Dependency Inversion Principle DIP)
高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。抽象不应该依赖于具体实现,具体实现应该依赖于抽象。就是你出国要说你是中国人,而不能说你是哪个村子的。比如说中国人是抽象的,下面有具体的xx省,xx市,xx县。你要依赖的抽象是中国人,而不是你是xx村的。 - 接口分离原则ISP(the Interface Segregation Principle ISP)
设计时采用多个与特定客户类有关的接口比采用一个通用的接口要好。就比如一个手机拥有打电话,看视频,玩游戏等功能,把这几个功能拆分成不同的接口,比在一个接口里要好的多。
类与接口
34 抽象类和接口的对比
- 抽象类是用来捕捉子类的通用特性的。接口是抽象方法的集合。
- 从设计层面来说,抽象类是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。
相同点:
- 接口和抽象类都不能实例化
- 都位于继承的顶端,用于被其他实现或继承
- 都包含抽象方法,其子类都必须覆写这些抽象方法
不同点:
35 普通类和抽象类有哪些区别?
- 普通类不能包含抽象方法,抽象类可以包含抽象方法。
- 抽象类不能直接实例化,普通类可以直接实例化。
36 抽象类能使用 final 修饰吗?
- 不能,定义抽象类就是让其他类继承的,如果定义为 final 该类就不能被继承,这样彼此就会产生矛盾,所以 final 不能修饰抽象类。
37 创建一个对象用什么关键字?对象实例与对象引用有何不同?
- new关键字,new创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。一个对象引用可以指向0个或1个对象(一根绳子可以不系气球,也可以系一个气球);一个对象可以有n个引用指向它(可以用n条绳子系住一个气球)。
变量与方法
38 成员变量与局部变量的区别有哪些?
- 变量:在程序执行的过程中,在某个范围内其值可以发生改变的量。从本质上讲,变量其实是内存中的一小块区域
- 成员变量:方法外部,类内部定义的变量
- 局部变量:类的方法中的变量
成员变量和局部变量的区别:
作用域
- 成员变量:针对整个类有效。
- 局部变量:只在某个范围内有效。(一般指的就是方法,语句体内)
存储位置
- 成员变量:随着对象的创建而存在,随着对象的消失而消失,存储在堆内存中。
- 局部变量:在方法被调用,或者语句被执行的时候存在,存储在栈内存中。当方法调用完,或者语句结束后,就自动释放。
生命周期
- 成员变量:随着对象的创建而存在,随着对象的消失而消失。
- 局部变量:当方法调用完,或者语句结束后,就自动释放。
初始值
- 成员变量:有默认初始值。
- 局部变量:没有默认初始值,使用前必须赋值。
39 在Java中定义一个不做事且没有参数的构造方法的作用?
- Java程序在执行子类的构造方法之前,如果没有用super()来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用super()来调用父类中特定的构造方法,则编译时将发生错误,因为Java程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。
40 在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是?
- 帮助子类做初始化工作。
41 一个类的构造方法的作用是什么?若一个类没有声明构造方法,改程序能正确执行吗?为什么?
- 主要作用是完成对类对象的初始化工作。可以执
- 行。因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。
42 构造方法有哪些特性?
- 名字与类名相同
- 没有返回值,但不能用void声明构造函数
- 生成类的对象时自动执行,无需调用
43 静态变量和实例变量区别?
- 静态变量: 静态变量由于不属于任何实例对象,属于类的,所以在内存中只会有一份,在类的加载过程中,JVM只为静态变量分配一次内存空间。
- 实例变量: 每次创建对象,都会为每个对象分配成员变量内存空间,实例变量是属于实例对象的,在内存中,创建几次对象,就有几份成员变量。
44 静态变量与普通变量区别?
- static变量也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。
- 还有一点就是static成员变量的初始化顺序按照定义的顺序进行初始化。
45 静态方法和实例方法有何不同?
静态方法和实例方法的区别主要体现在两个方面:
- 在外部调用静态方法时,可以使用"类名.方法名"的方式,也可以使用"对象名.方法名"的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。
- 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制。
重写与重载
46 构造器(constructor)是否可被重写(override)?
- 构造器不能被继承,因此不能被重写,但可以被重载。
47 重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分?
- 方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。、
- 重载:发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不同、顺序不同),与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分。
- 重写:发生在父子类中,方法名、参数列表必须相同,返回值小于等于父类,抛出的异常小于等于父类,访问修饰符大于等于父类(里氏代换原则);如果父类方法访问修饰符为private则子类中就不是重写。
对象相等判断
48 == 和 equals 的区别是什么?
-
== : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型 == 比较的是值,引用数据类型 == 比较的是内存地址)。
-
equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
- 情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
- 情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来两个对象的内容相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
举个例子:
public class test1 {
public static void main(String[] args) {
String a = new String("ab"); // a 为一个引用
String b = new String("ab"); // b为另一个引用,对象的内容一样
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 从常量池中查找
if (aa == bb) // true
System.out.println("aa==bb");
if (a == b) // false,非同一对象
System.out.println("a==b");
if (a.equals(b)) // true
System.out.println("aEQb");
if (42 == 42.0) { // true
System.out.println("true");
}
}
}
说明:
- String中的equals方法是被重写过的,因为object的equals方法是比较的对象的内存地址,而String的equals方法比较的是对象的值。
- 当创建String类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个String对象。
49 hashCode 与 equals (重要)?
hashCode 与 equals(为什么重写equals方法后, hashCode方法也必须重写)
值传递
50 当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递?
- 是值传递。Java 语言的方法调用只支持参数的值传递。当一个对象实例作为一个参数被传递到方法中时,参数的值就是对该对象的引用。对象的属性可以在被调用过程中被改变,但对对象引用的改变是不会影响到调用者的。
51 当为什么 Java 中只有值传递?
- 首先回顾一下在程序设计语言中有关将参数传递给方法(或函数)的一些专业术语。按值调用(call by value)表示方法接收的是调用者提供的值,而按引用调用(call by reference)表示方法接收的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。 它用来描述各种程序设计语言(不只是Java)中方法参数传递方式。
- Java程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝,也就是说,方法不能修改传递给它的任何参数变量的内容。
52 值传递和引用传递有什么区别?
- 值传递:指的是在方法调用时,传递的参数是按值的拷贝传递,传递的是值的拷贝,也就是说传递后就互不相关了。
- 引用传递:指的是在方法调用时,传递的参数是按引用进行传递,其实传递的引用的地址,也就是变量所对应的内存空间的地址。传递的是值的引用,也就是说传递前和传递后都指向同一个引用(也就是同一个内存空间)。
IO流
53 java 中 IO 流分为几种?
- 按照流的流向分,可以分为输入流和输出流。
- 按照操作单元划分,可以划分为字节流和字符流。
- 按照流的角色划分为节点流和处理流。
- InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
- OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
按操作方式分类结构图:
按操作对象分类结构图:
54 BIO,NIO,AIO 有什么区别?
简答:
- BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。
- NIO:Non IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。
- AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机制。
详细回答:
- BIO (Blocking I/O): 同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
- NIO (New I/O): NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应java.nio 包,提供了 Channel , Selector,Buffer等抽象。NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发
- AIO (Asynchronous I/O): AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。
55 Files的常用方法都有哪些?
- Files. exists():检测文件路径是否存在。
- Files. createFile():创建文件。
- Files. createDirectory():创建文件夹。
- Files. delete():删除一个文件或目录。
- Files. copy():复制文件。
- Files. move():移动文件。
- Files. size():查看文件个数。
- Files. read():读取文件。
- Files. write():写入文件。
反射
56 什么是反射机制?
-
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
-
静态编译和动态编译
- 静态编译:在编译时确定类型,绑定对象
- 动态编译:运行时确定类型,绑定对象
57 反射机制优缺点?
- 优点: 运行期类型的判断,动态加载类,提高代码灵活度。
- 缺点: 性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的java代码要慢很多。
59 反射机制的应用场景有哪些?
- 反射是框架设计的灵魂。
- 在我们平时的项目开发过程中,基本上很少会直接使用到反射机制,但这不能说明反射机制没有用,实际上有很多设计、开发都与反射机制有关,例如模块化的开发,通过反射去调用对应的字节码;动态代理设计模式也采用了反射机制,还有我们日常使用的 Spring/Hibernate 等框架也大量使用到了反射机制。
- 举例:①我们在使用JDBC连接数据库时使用Class.forName()通过反射加载数据库的驱动程序;②Spring框架也用到很多反射机制,最经典的就是xml的配置模式。Spring 通过 XML 配置模式装载 Bean 的过程:1) 将程序内所有 XML 或 Properties 配置文件加载入内存中; 2)Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息; 3)使用反射机制,根据这个字符串获得某个类的Class实例; 4)动态配置实例的属性。
60 Java获取反射的三种方法?
1.通过new对象实现反射机制 2.通过路径实现反射机制 3.通过类名实现反射机制
public class Student {
private int id;
String name;
protected boolean sex;
public float score;
}
public class Get {
//获取反射机制三种方式
public static void main(String[] args) throws ClassNotFoundException {
//方式一(通过建立对象)
Student stu = new Student();
Class classobj1 = stu.getClass();
System.out.println(classobj1.getName());
//方式二(所在通过路径-相对路径)
Class classobj2 = Class.forName("fanshe.Student");
System.out.println(classobj2.getName());
//方式三(通过类名)
Class classobj3 = Student.class;
System.out.println(classobj3.getName());
}
}
String相关
61 字符型常量和字符串常量的区别?
- 形式上: 字符常量是单引号引起的一个字符 字符串常量是双引号引起的若干个字符
- 含义上: 字符常量相当于一个整形值(ASCII值),可以参加表达式运算 字符串常量代表一个地址值(该字符串在内存中存放位置)
- 占内存大小 字符常量只占一个字节 字符串常量占若干个字节(至少一个字符结束标志)
63 什么是字符串常量池?
- 字符串常量池位于堆内存中,专门用来存储字符串常量,可以提高内存的使用率,避免开辟多块空间存储相同的字符串,在创建字符串时 JVM 会首先检查字符串常量池,如果该字符串已经存在池中,则返回它的引用,如果不存在,则实例化一个字符串放到池中,并返回其引用。
63 String 是最基本的数据类型吗?
- 不是。Java 中的基本数据类型只有 8 个 :byte、short、int、long、float、double、char、boolean;除了基本类型,剩下的都是引用类型,Java 5 以后引入的枚举类型也算是一种比较特殊的引用类型。
64 String有哪些特性?
- 不变性:String 是只读字符串,是一个典型的 immutable 对象,对它进行任何操作,其实都是创建一个新的对象,再把引用指向该对象。不变模式的主要作用在于当一个对象需要被多线程共享并频繁访问时,可以保证数据的一致性。
- 常量池优化:String 对象创建之后,会在字符串常量池中进行缓存,如果下次创建同样的对象时,会直接返回缓存的引用。
- final:使用 final 来定义 String 类,表示 String 类不能被继承,提高了系统的安全性。
65 String为什么是不可变的吗?
- 简单来说就是String类利用了final修饰的char类型数组存储字符,源码如下图所以:
/** The value is used for character storage. */ private final char value[];
66 是否可以继承 String 类?
- String 类是 final 类,不可以被继承。
67 String str="i"与 String str=new String(“i”)一样吗?
- 不一样,因为内存的分配方式不一样。String str="i"的方式,java 虚拟机会将其分配到常量池中;而 String str=new String(“i”) 则会被分到堆内存中。
68 String s = new String(“xyz”);创建了几个字符串对象?
-
List item两个对象,一个是静态区的"xyz",一个是用new创建在堆上的对象。
String str1 = “hello”; //str1指向静态区 String str2 = new String(“hello”);
//str2指向堆上的对象String str3 = “hello”; String str4 = new String(“hello”);
System.out.println(str1.equals(str2));//true
System.out.println(str2.equals(str4)); //true System.out.println(str1
== str3); //trueSystem.out.println(str1 == str2); //false System.out.println(str2 == str4); //falseSystem.out.println(str2 ==
“hello”); //false str2 = str1; System.out.println(str2 ==
“hello”);//true
69 String 类的常用方法都有那些?
- indexOf():返回指定字符的索引。
- charAt():返回指定索引处的字符。
- replace():字符串替换。
- trim():去除字符串两端空白。
- split():分割字符串,返回一个分割后的字符串数组。
- getBytes():返回字符串的 byte 类型数组。
- length():返回字符串长度。
- toLowerCase():将字符串转成小写字母。
- toUpperCase():将字符串转成大写字符。
- substring():截取字符串。
- equals():字符串比较。
70 在使用 HashMap 的时候,用 String 做 key 有什么好处?
- HashMap 内部实现是通过 key 的 hashcode 来确定 value 的存储位置,因为字符串是不可变的,所以当创建字符串时,它的 hashcode 被缓存下来,不需要再次计算,所以相比于其他对象更快。
71 String和StringBuffer、StringBuilder的区别是什么?String为什么是不可变的?
可变性
- String类中使用字符数组保存字符串,private final char value[],所以string对象是不可变的。StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串,char[] value,这两种对象都是可变的。
线程安全性
- String中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder是StringBuilder与StringBuffer的公共父类,定义了一些字符串的基本操作,如expandCapacity、append、insert、indexOf等公共方法。StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder并没有对方法进行加同步锁,所以是非线程安全的。
性能
- 每次对String 类型进行改变的时候,都会生成一个新的String对象,然后将指针指向新的String 对象。StringBuffer每次都会对StringBuffer对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用StirngBuilder 相比使用StringBuffer 仅能获得10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
对于三者使用的总结
- 如果要操作少量的数据用 = String
- 单线程操作字符串缓冲区 下操作大量数据 = StringBuilder
- 多线程操作字符串缓冲区 下操作大量数据 = StringBuffer
包装类相关
72 自动装箱与拆箱
- 装箱:将基本类型用它们对应的引用类型包装起来;
- 拆箱:将包装类型转换为基本数据类型;
73 int 和 Integer 有什么区别
- Java 是一个近乎纯洁的面向对象编程语言,但是为了编程的方便还是引入了基本数据类型,但是为了能够将这些基本数据类型当成对象操作,Java 为每一个基本数据类型都引入了对应的包装类型(wrapper class),int 的包装类就是 Integer,从 Java 5 开始引入了自动装箱/拆箱机制,使得二者可以相互转换。
- Java 为每个原始类型提供了包装类型:
原始类型: boolean,char,byte,short,int,long,float,double
包装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double
74 Integer a= 127 与 Integer b = 127相等吗
- 对于对象引用类型:==比较的是对象的内存地址。
- 对于基本数据类型:==比较的是值。
如果整型字面量的值在-128到127之间,那么自动装箱时不会new新的Integer对象,而是直接引用常量池中的Integer对象,超过范围 a1==b1的结果是false
public static void main(String[] args) {
Integer a = new Integer(3);
Integer b = 3; // 将3自动装箱成Integer类型
int c = 3;
System.out.println(a == b); // false 两个引用没有引用同一对象
System.out.println(a == c); // true a自动拆箱成int类型再和c比较
System.out.println(b == c); // true
Integer a1 = 128;
Integer b1 = 128;
System.out.println(a1 == b1); // false
Integer a2 = 127;
Integer b2 = 127;
System.out.println(a2 == b2); // true
}
集合容器概述
75 什么是集合
- 集合就是一个放数据的容器,准确的说是放数据对象引用的容器
- 集合类存放的都是对象的引用,而不是对象的本身
- 集合类型主要有3种:set(集)、list(列表)和map(映射)
76 集合的特点
- 集合的特点主要有如下两点:
- 集合用于存储对象的容器,对象是用来封装数据,对象多了需要存储集中式管理
- 和数组对比对象的大小不确定。因为集合是可变长度的。数组需要提前定义大小
77 集合和数组的区别
- 数组是固定长度的;集合可变长度的
- 数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型
- 数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型
78 使用集合框架的好处
- 容量自增长
- 提供了高性能的数据结构和算法,使编码更轻松,提高了程序速度和质量
- 可以方便地扩展或改写集合,提高代码复用性和可操作性
- 通过使用JDK自带的集合类,可以降低代码维护和学习新API成本
79 常用的集合类有哪些
Map接口和Collection接口是所有集合框架的父接口:
- Collection接口的子接口包括:Set接口和List接口
- Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等
- Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等
- List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等
80 List,Set,Map三者的区别
-
Java 容器分为 Collection 和 Map 两大类,Collection集合的子接口有Set、List、Queue三种子接口。我们比较常用的是Set、List,Map接口不是collection的子接口
-
Collection集合主要有List和Set两大接口
- List:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector
- Set:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及TreeSet
-
Map是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不要求有序,允许重复。Map没有继承于Collection接口,从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象
- Map 的常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、
ConcurrentHashMap
- Map 的常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、
81 集合框架底层数据结构
-
Collection
-
List
- Arraylist: Object数组
- Vector: Object数组
- LinkedList: 双向循环链表
-
Set
- HashSet(无序,唯一):基于 HashMap 实现的,底层采用 HashMap 来保存元素
- LinkedHashSet: LinkedHashSet 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一样,不过还是有一点点区别的
- TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树。)
-
-
Map
- HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间
- LinkedHashMap:LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑
- HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
- TreeMap: 红黑树(自平衡的排序二叉树)
82 哪些集合类是线程安全的
- Vector:就比Arraylist多了个 synchronized (线程安全),因为效率较低,现在已经不太建议使用
- HashTable:就比hashMap多了个synchronized (线程安全),不建议使用
- ConcurrentHashMap:是Java5中支持高并发、高吞吐量的线程安全HashMap实现。它由Segment数组结构和HashEntry数组结构组成。Segment数组在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键-值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构;一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素;每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。(推荐使用)
83 Java集合的快速失败机制 “fail-fast”
84 Array 和 ArrayList 有何区别
- Array 可以包含基本类型和对象类型, ArrayList 只能包含对象类型
- Array 大小是固定的, ArrayList 的大小是动态变化的
- 相比于 Array , ArrayList 有着更多的内置方法,如 addAll() , removeAll()
- 对于基本类型数据, ArrayList 使用自动装箱来减少编码工作量;而当处理固定大小的基本数据类型的时候,这种方式相对比较慢,这时候应该使用 Array
85 Collection 和 Collections 有什么区别
- Collection 是一个集合接口。它提供了对集合对象进行基本操作的通用接口方法
- Collections 是一个包装类。它包含有各种有关集合操作的静态多态方法,例如常用的 sort()方法。此类不能实例化,就像一个工具类,服务于Java的 Collection 框架
86 遍历一个 List 有哪些不同的方式
先说一下常见的元素在内存中的存储方式,主要有两种:
- 顺序存储(Random Access):相邻的数据元素在内存中的位置也是相邻的,可以根据元素的位置(如 ArrayList 中的下表)读取元素。
- 链式存储(Sequential Access):每个数据元素包含它下一个元素的内存地址,在内存中不要求相邻。例如 LinkedList
主要的遍历方式主要有三种:
- for 循环遍历:遍历者自己在集合外部维护一个计数器,依次读取每一个位置的元素
- Iterator 遍历:基于顺序存储集合的 Iterator 可以直接按位置访问数据。基于链式存储集合的Iterator ,需要保存当前遍历的位置,然后根据当前位置来向前或者向后移动指针
- foreach 遍历: foreach 内部也是采用了 Iterator 的方式实现,但使用时不需要显示地声明Iterator
List集合
87 ArrayList的扩容机制
jdk 7情况下:
ArrayList list = new ArrayList();//底层创建了长度是10的Object[]数组elementData
list.add(123);//elementData[0] = new Integer(123);
…
list.add(11);//如果此次的添加导致底层elementData数组容量不够,则扩容。
默认情况下,扩容为原来的容量的1.5倍,同时需要将原有数组中的数据复制到新的数组中。
jdk 8中ArrayList的变化:
ArrayList list = new ArrayList();//底层Object[] elementData初始化为{}.并没创建长度为10的数组
list.add(123);//第一次调用add()时,底层才创建了长度10的数组,并将数据123添加到elementData[0]
…
后续的添加和扩容操作与jdk 7 无异
小结:jdk7中的ArrayList的对象的创建类似于单例的饿汉式,而jdk8中的ArrayList的对象的创建类似于单例的懒汉式,延迟了数组的创建,节省内存。
88 ArrayList 和 LinkedList 的区别是什么
- 是否线程安全: ArrayList 和 LinkedList 都是不保证线程安全的
- 底层实现: ArrayList 的底层实现是数组, LinkedList 的底层是双向链表
- 内存占用: ArrayList 会存在一定的空间浪费,因为每次扩容都是之前的1.5倍,而 LinkedList中的每个元素要存放直接后继和直接前驱以及数据,所以对于每个元素的存储都要比 ArrayList花费更多的空间
- 应用场景: ArrayList 的底层数据结构是数组,所以在插入和删除元素时的时间复杂度都会收到位置的影响,平均时间复杂度为o(n),在读取元素的时候可以根据下标直接查找到元素,不受位置的影响,平均时间复杂度为o(1),所以 ArrayList 更加适用于多读,少增删的场景。 LinkedList的底层数据结构是双向链表,所以插入和删除元素不受位置的影响,平均时间复杂度为o(1),如果是在指定位置插入则是o(n),因为在插入之前需要先找到该位置,读取元素的平均时间复杂度为o(n)。所以 LinkedList 更加适用于多增删,少读写的场景
89 ArrayList 和 Vector 的区别是什么
- 相同点
1. 都实现了 List 接口
2. 底层数据结构都是数组 - 不同点
1. 线程安全:Vector 使用了 Synchronized 来实现线程同步,所以是线程安全的,而ArrayList 是线程不安全的
2. 性能:由于 Vector 使用了 Synchronized 进行加锁,所以性能不如 ArrayList
3. 扩容: ArrayList 和 Vector 都会根据需要动态的调整容量,但是 ArrayList 每次扩容为旧容量的1.5倍,而 Vector 每次扩容为旧容量的2倍
90 简述 ArrayList、Vector、LinkedList 的存储性能和特性
- ArrayList 底层数据结构为数组,对元素的读取速度快,而增删数据慢,线程不安全
- LinkedList 底层为双向链表,对元素的增删数据快,读取慢,线程不安全
- Vector 的底层数据结构为数组,用 Synchronized 来保证线程安全,性能较差,但线程安全
Set集合
91 说一下 HashSet 的实现原理
HashSet 的底层是 HashMap ,默认构造函数是构建一个初始容量为16,负载因子为0.75 的 HashMap 。HashSet 的值存放于 HashMap 的 key 上, HashMap 的 value 统一为 PRESENT 。
92 HashSet如何检查重复?
这里面涉及到了 HasCode() 和 equals() 两个方法。
- equals()
先看下 String 类中重写的 equals 方法。
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
从源码中可以看到:
-
equals 方法首先比较的是内存地址,如果内存地址相同,直接返回 true ;如果内存地址不同,再比较对象的类型,类型不同直接返回 false ;类型相同,再比较值是否相同;值相同返回 true ,值不同返回 false 。总结一下, equals 会比较内存地址、对象类型、以及值,内存地址相同, equals 一定返回 true ;对象类型和值相同, equals 方法一定返回 true 。
-
如果没有重写 equals 方法,那么 equals 和 == 的作用相同,比较的是对象的地址值。
-
hashCode
hashCode 方法返回对象的散列码,返回值是 int 类型的散列码。散列码的作用是确定该对象在哈希表中的索引位置。
关于 hashCode 有一些约定
- 两个对象相等,则 hashCode 一定相同
- 两个对象有相同的 hashCode 值,它们不一定相等
- hashCode() 方法默认是对堆上的对象产生独特值,如果没有重写 hashCode() 方法,则该类的两个对象的 hashCode 值肯定不同
介绍完equals()方法和hashCode()方法,继续说下HashSet是如何检查重复的。
HashSet 的特点是存储元素时无序且唯一,在向 HashSet 中添加对象时,首相会计算对象的 HashCode值来确定对象的存储位置,如果该位置没有其他对象,直接将该对象添加到该位置;如果该存储位置有存储其他对象(新添加的对象和该存储位置的对象的 HashCode 值相同),调用 equals 方法判断两个对象是否相同,如果相同,则添加对象失败,如果不相同,则会将该对象重新散列到其他位置。
93 HashSet与HashMap的区别
HashMap | HashSet |
---|---|
实现了 Map 接口 | 实现了 Set 接口 |
存储键值对 | 存储对象 |
key 唯一, value 不唯一 | 存储对象唯一 |
HashMap 使用键( Key )计算 Hashcode | HashSet 使用成员对象来计算 hashcode 值 |
速度相对较快 | 速度相对较慢 |
Map集合
94 HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底层实现
- JDK1.7的底层数据结构(数组+链表)
- JDK1.8的底层数据结构(数组+链表)
HashMap在JDK1.7和JDK1.8中有哪些不同点:
95 HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底层实现
因为 HashMap 是通过 key 的hash值来确定存储的位置,但Hash值的范围是-2147483648到2147483647,不可能建立一个这么大的数组来覆盖所有hash值。所以在计算完hash值后会对数组的长度进行取余操作,如果数组的长度是2的幂次方, (length - 1)&hash 等同于 hash%length ,可以用(length - 1)&hash 这种位运算来代替%取余的操作进而提高性能。
96 HashMap的put方法的具体流程
97 HashMap的扩容操作是怎么实现的?
- 初始值为16,负载因子为0.75,阈值为负载因子*容量
- resize() 方法是在 hashmap 中的键值对大于阀值时或者初始化时,就调用 resize() 方法进行扩容
- 每次扩容,容量都是之前的两倍
- 扩容时有个判断 e.hash & oldCap 是否为零,也就是相当于hash值对数组长度的取余操作,若等于0,则位置不变,若等于1,位置变为原位置加旧容量
98 HashMap默认加载因子为什么选择0.75?
这个主要是考虑空间利用率和查询成本的一个折中。如果加载因子过高,空间利用率提高,但是会使得哈希冲突的概率增加;如果加载因子过低,会频繁扩容,哈希冲突概率降低,但是会使得空间利用率变低。具体为什么是0.75,不是0.74或0.76,这是一个基于数学分析(泊松分布)和行业规定一起得到的一个结论。
99 为什么要将链表中转红黑树的阈值设为8?为什么不一开始直接使用红黑树?
- 因为红黑树的节点所占的空间是普通链表节点的两倍,但查找的时间复杂度低,所以只有当节点特别多时,红黑树的优点才能体现出来。至于为什么是8,是通过数据分析统计出来的一个结果,链表长度到达8的概率是很低的,综合链表和红黑树的性能优缺点考虑将大于8的链表转化为红黑树
- 链表转化为红黑树除了链表长度大于8,还要 HashMap 中的数组长度大于64。也就是如果HashMap 长度小于64,链表长度大于8是不会转化为红黑树的,而是直接扩容
100 HashMap是怎么解决哈希冲突的?
哈希冲突: hashMap 在存储元素时会先计算 key 的hash值来确定存储位置,因为 key 的hash值计算最后有个对数组长度取余的操作,所以即使不同的 key 也可能计算出相同的hash值,这样就引起了hash冲突。 hashMap 的底层结构中的链表/红黑树就是用来解决这个问题的。
- 拉链法: HasMap 中的数据结构为数组+链表/红黑树,当不同的 key 计算出的hash值相同时,就用链表的形式将Node结点(冲突的 key 及 key 对应的 value )挂在数组后面
- hash函数: key 的hash值经过两次扰动, key 的 hashCode 值与 key 的 hashCode 值的右移16位进行异或,然后对数组的长度取余(实际为了提高性能用的是位运算,但目的和取余一样),这样做可以让hashCode 取值出的高位也参与运算,进一步降低hash冲突的概率,使得数据分布更平均
- 红黑树: 在拉链法中,如果hash冲突特别严重,则会导致数组上挂的链表长度过长,性能变差,因此在链表长度大于8时,将链表转化为红黑树,可以提高遍历链表的速度
101 HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?
hashCode() 处理后的哈希值范围太大,不可能在内存建立这么大的数组。
102 能否使用任何类作为 Map 的 key?
可以,但要注意以下两点:
- 如果类重写了 equals() 方法,也应该重写 hashCode() 方法
- 最好定义 key 类是不可变的,这样 key 对应的 hashCode() 值可以被缓存起来,性能更好,这也是为什么 String 特别适合作为 HashMap 的 key
103 为什么HashMap中String、Integer这样的包装类适合作为Key?
- 这些包装类都是 final 修饰,是不可变性的, 保证了 key 的不可更改性,不会出现放入和获取时哈希值不同的情况
- 它们内部已经重写过 hashcode() , equal() 等方法
104 HashMap 多线程导致死循环问题
由于JDK1.7的 hashMap 遇到hash冲突采用的是头插法,在多线程情况下会存在死循环问题,但JDK1.8已经改成了尾插法,不存在这个问题了。但需要注意的是JDK1.8中的 HashMap 仍然是不安全的,在多线程情况下使用仍然会出现线程安全问题。
105 ConcurrentHashMap 底层具体实现
- JDK1.7
在JDK1.7中, ConcurrentHashMap 采用 Segment 数组 + HashEntry 数组的方式进行实现。Segment 实现了 ReentrantLock ,所以 Segment 有锁的性质, HashEntry 用于存储键值对。一个 ConcurrentHashMap 包含着一个 Segment 数组,一个 Segment 包含着一个 HashEntry 数组,HashEntry 是一个链表结构,如果要获取 HashEntry 中的元素,要先获得 Segment 的锁。
- JDK1.8
在JDK1.8中,不在是 Segment + HashEntry 的结构了,而是和 HashMap 类似的结构,Node数组+链表/红黑树,采用 CAS + synchronized 来保证线程安全。当链表长度大于8,链表转化为红黑树。在JDK1.8中 synchronized 只锁链表或红黑树的头节点,是一种相比于 segment 更为细粒度的锁,锁的竞争变小,所以效率更高。
总结一下:
- JDK1.7底层是 ReentrantLock + Segment + HashEntry ,JDK1.8底层是 synchronized + CAS +链 表/红黑树
- JDK1.7采用的是分段锁,同时锁住几个 HashEntry ,JDK1.8锁的是Node节点,只要没有发生哈希冲突,就不会产生锁的竞争。所以JDK1.8相比于JDK1.7提供了一种粒度更小的锁,减少了锁的竞争,提高了 ConcurrentHashMap 的并发能力
106 HashTable的底层实现
HashTable 的底层数据结构是数组+链表,链表主要是为了解决哈希冲突,并且整个数组都是synchronized 修饰的,所以 HashTable 是线程安全的,但锁的粒度太大,锁的竞争非常激烈,效率很低。
107 HashMap、ConcurrentHashMap及Hashtable 的区别
Java集合的常用方法
108 Collection常用方法
109 List常用方法
110 LinkedList常用方法
111 Map常用方法
112 Stack常用方法
113 Queue
Java异常概述
异常主要分为 Error 和 Exception 两种
- Error: Error 类以及他的子类的实例,代表了JVM本身的错误。错误不能被程序员通过代码处理
- EXception: Exception 以及他的子类,代表程序运行时发送的各种不期望发生的事件。可以被Java异常处理机制使用,是异常处理的核心
114 Java异常架构
115 Error(错误)
- 定义: Error 类及其子类。程序中无法处理的错误,表示运行应用程序中出现了严重的错误
- 特点: 此类错误一般表示代码运行时 JVM 出现问题。通常有 Virtual MachineError(虚拟机运行错误)、NoClassDefFoundError(类定义错误)等。比如 OutOfMemoryError: 内存不足错误;StackOverflowError: 栈溢出错误。此类错误发生时,JVM 将终止线程
- 这些错误是不受检异常,非代码性错误。因此,当此类错误发生时,应用程序不应该去处理此类错误。按照Java惯例,我们是不应该实现任何新的Error子类的!
116 Exception(异常)
- 程序本身可以捕获并且可以处理的异常。Exception 这种异常又分为两类:运行时异常和编译时异常
运行时异常
- 定义: RuntimeException 类及其子类,表示 JVM 在运行期间可能出现的异常
- 特点: Java 编译器不会检查它。也就是说,当程序中可能出现这类异常时,倘若既"没有通过throws声明抛出它",也"没有用try-catch语句捕获它",还是会编译通过。比如
NullPointerException空指针异常、ArrayIndexOutBoundException数组下标越界异常、
ClassCastException类型转换异常、ArithmeticExecption算术异常。此类异常属于不受检异常,一般是由程序逻辑错误引起的,在程序中可以选择捕获处理,也可以不处理。虽然 Java 编译器不会检查运行时异常,但是我们也可以通过 throws 进行声明抛出,也可以通过 try-catch 对它进行捕获处理。如果产生运行时异常,则需要通过修改代码来进行避免。例如,若会发生除数为零的情况,则需要通过代码避免该情况的发生 - RuntimeException 异常会由 Java 虚拟机自动抛出并自动捕获(就算我们没写异常捕获语句运行时也会抛出错误!!),此类异常的出现绝大数情况是代码本身有问题应该从逻辑上去解决并改进代码
编译时异常
- 定义: Exception 中除 RuntimeException 及其子类之外的异常
- 特点: Java 编译器会检查它。如果程序中出现此类异常,比如ClassNotFoundException(没有找到指定的类异常),IOException(IO流异常),要么通过throws进行声明抛出,要么通过try-catch进行捕获处理,否则不能通过编译。在程序中,通常不会自定义该类异常,而是直接使用系统提供的异常类。该异常我们必须手动在代码里添加捕获语句来处理该异常
117 受检异常与非受检异常
- Java 的所有异常可以分为受检异常(checked exception)和非受检异常(unchecked exception)
受检异常
- 编译器要求必须处理的异常。正确的程序在运行过程中,经常容易出现的、符合预期的异常情况。一旦发生此类异常,就必须采用某种方式进行处理。除 RuntimeException 及其子类外,其他的Exception 异常都属于受检异常。编译器会检查此类异常,也就是说当编译器检查到应用中的某处可能会此类异常时,将会提示你处理本异常——要么使用try-catch捕获,要么使用方法签名中用 throws 关键字抛出,否则编译不通过
非受检异常
- 编译器不会进行检查并且不要求必须处理的异常,也就说当程序中出现此类异常时,即使我们没有try-catch捕获它,也没有使用throws抛出该异常,编译也会正常通过。该类异常包括运行时异常(RuntimeException极其子类)和错误(Error)
118 Java异常处理结构图
Java异常常见面试题
119 Error 和 Exception 区别是什么?
- Error 类型的错误通常为虚拟机相关错误,如系统崩溃,内存不足,堆栈溢出等,编译器不会对这类错误进行检测,JAVA 应用程序也不应对这类错误进行捕获,一旦这类错误发生,通常应用程序会被终止,仅靠应用程序本身无法恢复
- Exception 类的错误是可以在应用程序中进行捕获并处理的,通常遇到这种错误,应对其进行处理,使应用程序可以继续正常运行
120 运行时异常和一般异常(受检异常)区别是什么?
- 运行时异常包括 RuntimeException 类及其子类,表示 JVM 在运行期间可能出现的异常。 Java 编译器不会检查运行时异常
- 受检异常是Exception 中除 RuntimeException 及其子类之外的异常。 Java 编译器会检查受检异常
- RuntimeException异常和受检异常之间的区别:是否强制要求调用者必须处理此异常,如果强制要求调用者必须进行处理,那么就使用受检异常,否则就选择非受检异常,一般来讲,如果没有特殊的要求,我们建议使用RuntimeException异常
121 JVM 是如何处理异常的?
- 在一个方法中如果发生异常,这个方法会创建一个异常对象,并转交给 JVM,该异常对象包含异常名称,异常描述以及异常发生时应用程序的状态。创建异常对象并转交给 JVM 的过程称为抛出异常。可能有一系列的方法调用,最终才进入抛出异常的方法,这一系列方法调用的有序列表叫做调用栈。
- JVM 会顺着调用栈去查找看是否有可以处理异常的代码,如果有,则调用异常处理代码。当 JVM发现可以处理异常的代码时,会把发生的异常传递给它。如果 JVM 没有找到可以处理该异常的代码块,JVM 就会将该异常转交给默认的异常处理器(默认处理器为 JVM 的一部分),默认异常处理器打印出异常信息并终止应用程序。
122 throw 和 throws 的区别是什么?
- Java 中的异常处理除了包括捕获异常和处理异常之外,还包括声明异常和拋出异常,可以通过throws 关键字在方法上声明该方法要拋出的异常,或者在方法内部通过 throw 拋出异常对象。
throws 关键字和 throw 关键字在使用上的几点区别如下:
- throw 关键字用在方法内部,只能用于抛出一种异常,用来抛出方法或代码块中的异常,受查异常和非受查异常都可以被抛出
- throws 关键字用在方法声明上,可以抛出多个异常,用来标识该方法可能抛出的异常列表。一个方法用 throws 标识了可能抛出的异常列表,调用该方法的方法中必须包含可处理异常的代码,否则也要在方法签名中用 throws 关键字声明相应的异常
123 try-catch-finally 中,如果 catch 中 return 了,finally 还会执行吗?
- 答: 会执行,在 return 前执行。
- 注意: 在 finally 中改变返回值的做法是不好的,因为如果存在 finally 代码块,try中的 return 语句不会立马返回调用者,而是记录下返回值待 finally 代码块执行完毕之后再向调用者返回其值,然后如果在 finally 中修改了返回值,就会返回修改后的值。显然,在 finally 中返回或者修改返回值会对程序造成很大的困扰,C#中直接用编译错误的方式来阻止程序员干这种龌龊的事情,Java中也可以通过提升编译器的语法检查级别来产生警告或错误。
代码示例1:
public static int getInt() {
int a = 10;
try {
System.out.println(a / 0);
a = 20;
} catch (ArithmeticException e) {
a = 30;
return a;
/*
* return a 在程序执行到这一步的时候,这里不是return a 而是 return 30;这个返回
路径就形成了
* 但是呢,它发现后面还有finally,所以继续执行finally的内容,a=40
* 再次回到以前的路径,继续走return 30,形成返回路径之后,这里的a就不是a变量了,而是
常量30
*/
} finally {
a = 40;
}
return a;
}
执行结果:30
代码示例2:
public static int getInt() {
int a = 10;
try {
System.out.println(a / 0);
a = 20;
} catch (ArithmeticException e) {
a = 30;
return a;
} finally {
a = 40;
//如果这样,就又重新形成了一条返回路径,由于只能通过1个return返回,所以这里直接返回40
return a;
}
}
执行结果:40、
124 常见的 RuntimeException 有哪些?
- ClassCastException(类转换异常)
- IndexOutOfBoundsException(数组越界)
- NullPointerException(空指针)
- ArrayStoreException(数据存储异常,操作数组时类型不一致)
- 还有IO操作的BufferOverflowException异常
125 Java常见异常有哪些?
- java.lang.IllegalAccessError: 违法访问错误。当一个应用试图访问、修改某个类的域(Field)或者调用其方法,但是又违反域或方法的可见性声明,则抛出该异常
- java.lang.InstantiationError: 实例化错误。当一个应用试图通过Java的new操作符构造一个抽象类或者接口时抛出该异常
- java.lang.OutOfMemoryError: 内存不足错误。当可用内存不足以让Java虚拟机分配给一个对象时抛出该错误
- java.lang.StackOverflowError: 堆栈溢出错误。当一个应用递归调用的层次太深而导致堆栈溢出或者陷入死循环时抛出该错误
- java.lang.ClassCastException: 类造型异常。假设有类A和B(A不是B的父类或子类),O是A的实例,那么当强制将O构造为类B的实例时抛出该异常。该异常经常被称为强制类型转换异常
- java.lang.ClassNotFoundException: 找不到类异常。当应用试图根据字符串形式的类名构造类,而在遍历CLASSPAH之后找不到对应名称的class文件时,抛出该异常
- java.lang.ArithmeticException: 算术条件异常。譬如:整数除零等
- java.lang.ArrayIndexOutOfBoundsException: 数组索引越界异常。当对数组的索引值为负数或大于等于数组大小时抛出
- java.lang.IndexOutOfBoundsException: 索引越界异常。当访问某个序列的索引值小于0或大于等于序列大小时,抛出该异常
- java.lang.InstantiationException: 实例化异常。当试图通过newInstance()方法创建某个类的实例,而该类是一个抽象类或接口时,抛出该异常
- java.lang.NoSuchFieldException: 属性不存在异常。当访问某个类的不存在的属性时抛出该异常
- java.lang.NoSuchMethodException: 方法不存在异常。当访问某个类的不存在的方法时抛出该异常
- java.lang.NullPointerException: 空指针异常。当应用试图在要求使用对象的地方使用了null时,抛出该异常。譬如:调用null对象的实例方法、访问null对象的属性、计算null对象的长度、使用throw语句抛出null等等
- java.lang.NumberFormatException: 数字格式异常。当试图将一个String转换为指定的数字类型,而该字符串确不满足数字类型要求的格式时,抛出该异常
- java.lang.StringIndexOutOfBoundsException: 字符串索引越界异常。当使用索引值访问某个字符串中的字符,而该索引值小于0或大于等于序列大小时,抛出该异常
多线程和并发编程
126 线程和进程的区别是什么?
定义: 线程是处理器任务调度和执行的基本单位;进程是操作系统资源分配的基本单位。
包含关系: 一个进程可以包含多个线程。
从Java虚拟机的角度来理解: Java虚拟机的运行时数据区包含堆、方法区、虚拟机栈、本地方法栈、程序计数器。各个进程之间是相互独立的,每个进程会包含多个线程,每个进程所包含的多个线程并不是相互独立的,这个线程会共享进程的堆和方法区,但这些线程不会共享虚拟机栈、本地方法栈、程序计数器。即每个进程所包含的多个线程共享进程的堆和方法区,并且具备私有的虚拟机栈、本地方法栈、程序计数器,如图所示,假设某个进程包含三个线程。
由上面可知以下进程和线程在以下几个方面的区别:
内存分配: 进程之间的地址空间和资源是相互独立的,同一个进程之间的线程会共享线程的地址空间和资源(堆和方法区)。
资源开销: 每个进程具备各自的数据空间,进程之间的切换会有较大的开销。属于同一进程的线程会共享堆和方法区,同时具备私有的虚拟机栈、本地方法栈、程序计数器,线程之间的切换资源开销较小。
127 线程死锁是如何产生的,如何避免?
内容摘自王道考研:
死锁(什么是死锁、怎样预防死锁、避免死锁、检测和解除)
128 Java 实现线程有哪几种方式?
- 继承 Thread 类创建线程
- 实现 Runnable 接口创建线程
- 使用 Callable 和 Future 创建线程
- 使用线程池例如用 Executor 框架
129 runnable 和 callable 有什么区别?
相同点:
- 两者都是接口
- 两者都需要调用 Thread.start 启动线程
不同点:
- callable的核心是 call() 方法,允许返回值, runnable 的核心是 run() 方法,没有返回值
- call() 方法可以抛出异常,但是 run() 方法不行
- callable 和 runnable 都可以应用于 executors , thread 类只支持 runnable
130 线程的run()和start()有什么区别?
- 线程是通过 Thread 对象所对应的方法 run() 来完成其操作的,而线程的启动是通过 start() 方法执行的
- run() 方法可以重复调用, start() 方法只能调用一次
131 为什么调用start()方法时会执行run()方法,而不直接执行run()方法?
start() 方法来启动线程,真正实现了多线程运行,这时无需等待 run() 方法体代码执行完毕而直接继续执行下面的代码。通过调用Thread类的 start() 方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行 run() 方法,这里方法 run() 称为线程体,它包含了要执行的这个线程的内容, run() 方法运行结束,此线程随即终止。
run() 方法只是类的一个普通方法而已,如果直接调用 run 方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待 run() 方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。
调用 start() 方法可以开启一个线程,而 run() 方法只是thread类中的一个普通方法,直接调用run() 方法还是在主线程中执行的。
132一个线程的生命周期有哪几种状态?它们之间如何流转的?
- 新建状态(NEW): 毫无疑问表示的是刚创建的线程,还没有开始启动
- 就绪状态(RUNNABLE): 当线程对象调用了 start()方法之后,该线程处于就绪状态。Java 虚拟机会为其创建方法调用栈和 程序计数器,等待调度运行
- 运行状态(RUNNING): 如果处于就绪状态的线程获得了 CPU,开始执行 run()方法的线程执行体,则该线程处于运行状态
- 阻塞状态(BLOCKED): 表示线程阻塞,等待获取锁,如碰到 synchronized、lock 等关键字等占用临界区的情况,一旦获取到锁就进行 RUNNABLE 状态继续运行
- 线程死亡(DEAD): 表示线程执行完毕后,进行终止状态。需要注意的是,一旦线程通过 start 方法启动后就再也不能回到初始 NEW 状态,线程终止后也不能再回到
RUNNABLE 状态
进程的五种基本状态及其转换:
133 线程同步以及线程调度相关的方法有哪些?
- wait(): 使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁
- sleep(): 使当前线程进入指定毫秒数的休眠,暂停执行
- notify(): 唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关
- notifyAll(): 唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态
- jion(): 与 sleep() 方法一样,是一个可中断的方法,在一个线程中调用另一个线程的 join()方法,会使得当前的线程挂起,知直到执行 join() 方法的线程结束。例如在B线程中调用A线程的join() 方法,B线程进入阻塞状态,直到A线程结束或者到达指定的时间
- yield(): 提醒调度器愿意放弃当前的CPU资源,使得当前线程从 RUNNING 状态切换到 RUNABLE状态
134 sleep()方法和wait()方法的区别?
相同点:
- wait() 方法和 sleep() 方法都可以使得线程进入到阻塞状态
- wait() 和 sleep() 方法都是可中断方法,被中断后都会收到中断异常
不同点:
- wait() 是Object的方法, sleep() 是Thread的方法
- wait() 必须在同步方法中进行, sleep() 方法不需要
- 线程在同步方法中执行 sleep() 方法,不会释放monitor的锁,而 wait() 方法会释放monitor的锁
- sleep() 方法在短暂的休眠之后会主动退出阻塞,而 wait() 方法在没有指定wait时间的情况下需要被其他线程中断才可以退出阻塞
135 线程的sleep()方法和yield()方法有什么不同?
- sleep() 方法会使得当前线程暂停指定的时间,没有消耗CPU时间片
- sleep() 使得线程进入到阻塞状态, yield() 只是对CPU进行提示,如果CPU没有忽略这个提示,会使得线程上下文的切换,进入到就绪状态
- sleep() 一定会完成给定的休眠时间, yield() 不一定能完成
- sleep() 需要抛出InterruptedException,而 yield() 方法无需抛出异常
136 线程通信的方法有哪些?
- 锁与同步
- wait() / notify() 或 notifyAll()
- 信号量
- 管道
137什么是线程同步?什么是线程互斥?他们是如何实现的?
- 线程的互斥是指某一个资源只能被一个访问者访问,具有唯一性和排他性。但访问者对资源访问的顺序是乱序的
- 线程的同步是指在互斥的基础上使得访问者对资源进行有序访问
线程同步的实现方法:
- 同步方法
- 同步代码块
- wait() 和 notify()
- Synchronized 关键字
- Lock 锁实现
- 分布式锁等
138 三个线程T1、T2、T3,如何让他们按顺序执行?
基本思路就是线程A、线程B、线程C三个线程同时启动,因为变量 num 的初始值为 0 ,所以线程B或线程C拿到锁后,进入 while() 循环,然后执行 wait() 方法,线程线程阻塞,释放锁。只有线程A拿到锁后,不进入 while() 循环,执行 num++ ,打印字符 A ,最后唤醒线程B和线程C。此时 num 值为 1 ,只有线程B拿到锁后,不被阻塞,执行 num++ ,打印字符 B ,最后唤醒线程A和线程C,后面以此类推。
常用方法:
- synchronized: 使用 synchronized关键字,具体的实现步骤请看基础知识
- join() 方法: 在A线程中调用了B线程的join()方法时,表示只有当B线程执行完毕时,A线程才能继续执行。基于这个原理,我们使得三个线程按顺序执行,然后循环多次即可。
- Lock锁: 该方法很容易理解,其实现代码和synchronized+wait/notify方法的很像。不管哪个线程拿到锁,只有符合条件的才能打印。
139 为什么要使用线程池?
我们知道不用线程池的话,每个线程都要通过 new Thread(xxRunnable).start()的方
式来创建并运行一个线程,线程少的话这不会是问题,而真实环境可能会开启多个线程让系统和程序达到最佳效率,当线程数达到一定数量就会耗尽系统的 CPU 和内存资源,也会造成 GC频繁收集和停顿,因为每次创建和销毁一个线程都是要消耗系统资源的,如果为每个任务都创建线程这无疑是一个很大的性能瓶颈。所以,线程池中的线程复用极大节省了系统资源,当线程一段时间不再有任务处理时它也会自动销毁,而不会长驻内存。
多线程其他知识补充
ThreadLocal
什么是ThreadLocal?有哪些应用场景?
ThreadLocal 是 JDK java.lang 包下的一个类, ThreadLocal 为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量,并且不会和其他线程的局部变量冲突,实现了线程间的数据隔离。
ThreadLocal 的应用场景主要有以下几个方面:
- 保存线程上下文信息,在需要的地方可以获取
- 线程间数据隔离
- 数据库连接
ThreadLocal 的原理可以概括为下图:
从上图可以看出每个线程都有一个 ThreadLocalMap , ThreadLocalMap 中保存着所有的
ThreadLocal ,而 ThreadLocal 本身只是一个引用本身并不保存值,值都是保存在 ThreadLocalMap中的,其中 ThreadLocal 为 ThreadLocalMap 中的 key 。其中图中的虚线表示弱引用。
这里简单说下Java中的引用类型,Java的引用类型主要分为强引用、软引用、弱引用和虚引用。
- 强引用:发生 gc 的时候不会被回收。
- 软引用:有用但不是必须的对象,在发生内存溢出之前会被回收。
- 弱引用:有用但不是必须的对象,在下一次GC时会被回收。
- 虚引用:无法通过虚引用获得对象,虚引用的用途是在 gc 时返回一个通知。
更多知识请阅读:详解JVM垃圾回收(Minor GC和Full GC有什么不同?、CMS收集器、G1收集器、垃圾回收算法)
为什么ThreadLocal会发生内存泄漏呢?
因为 ThreadLocal 中的 key 是弱引用,而 value 是强引用。当 ThreadLocal 没有被强引用时,在进行垃圾回收时, key 会被清理掉,而 value 不会被清理掉,这时如果不做任何处理, value 将永远不会被回收,产生内存泄漏。
如何解决ThreadLocal的内存泄漏?
其实在 ThreadLocal 在设计的时候已经考虑到了这种情况,在调用 set() 、 get() 、 remove() 等方法时就会清理掉 key 为 null 的记录,所以在使用完 ThreadLocal 后最好手动调用 remove() 方法。