近日复习的一些面试题,在网上收集了一些资料做的一些总结,希望能帮助到一些小伙伴。
Java基础
1、面向对象和面向过程的区别
面向对象: 性能比面向过程低。易维护、易复用、易扩展。面向对象有封装、继承、多态的特性,所以可以设计出低耦合的系统,使系统更加灵活、更易于维护。
面向过程: 性能比面向对象高。但没有易维护、易复用、易扩展。类调用需要实例化,开销比较大,比较消耗资源,所以性能是最重要的考量因素,比如单片机、嵌入式开发、linux/Unix等一般采用面向过程开发。
2、JDK、JRE和JVM的区别与关系
JDK(Java Developement Kit):是java开发工具包;包含JRE、Java开发工具;拥有开发和运行java程序的能力;面向程序开发人员。
JRE(Java Runtime Enviroment):是Java运行时环境;包含JVM、Java类库,java命令和其他一些基础构件;拥有运行java程序的能力;面向程序使用者。
JVM(Java Virtual Machine):是java虚拟机;是运⾏ Java 字节码的虚拟机;拥有跨平台运行的能力;JVM 有针对不同系统的特定实现(Windows,Linux,macOS),⽬的是使⽤相同的字节码,它们都会给出相同的结果。
3、.java文件运行的过程
Java源文件通过编译器生成相应的.class文件;.class文件通过Java虚拟机上的解释器编译成特定机器上的机器码。
① Java 源文件—->编译器—->字节码文件(.class文件)
② 字节码文件—->JVM—->机器码——>CPU执行
4、java内部类的理解
内部类: 定义在类中的类。包含静态内部类、成员内部类、局部内部类、匿名内部类。
静态内部类: 定义在类内部的静态类。
成员内部类: 定义在类内部的非静态类。
局部内部类: 定义在方法中的类。如果一个类只在方法使用,则可以考虑使用局部类。
匿名内部类: 匿名内部类是直接使用new来生成一个对象的引用。
5、详细介绍类的加载过程
过程:加载——》验证——》准备——》解析——》初始化——》使用——》卸载
一个类是如何加载的: 类的加载指的是将类的.class文件中的二进制数据读到内存中,将它放在运行数据区的方法区内,然后再堆内创建一个java.lang.Class对象用来封装在方法区类的数据区,并且会给Java程序员提供访问方法区内类的数据结构的,最后加载到堆区的Class对象。
类加载方式:
- 通过命令行启动应用由JVM初始化加载
- 通过Class.forName()方法动态加载
- 通过ClassLoader.loadClass()方法动态加载
6、类的构造方法的作用
主要作⽤是完成对类对象的初始化⼯作。一个类即使没有声明构造方法也会默认不带参数的构造方法。
构造⽅法有哪些特性?
- 名字与类名相同。
- 没有返回值,不能⽤ void 声明构造函数,。
- ⽣成类的对象时⾃动执⾏,无需调用。
7、面向对象的特征有哪些?
封装: 把抽象的事物抽象成一个对象,并对其对象的属性私有化,同时会提供一些能被外界访问的方法,这样一个对象才会有意义。
继承: 在父类的基础上,建立新的属性和方法,同时该类拥有复用父类的属性与功能。
多态: 同一行为具有不同的表现形式或形态的能力。
Java与C++区别:
相同点:都是面向对象语言,且都支持封装、继承、多态
不同点:C++支持多继承,有指针概念,需要程序员自己管理内存。Java是单继承,但是可以用接口实现多继承,Java不提供指针访问内存,程序内存更加安全,并且Java有JVM自动内存管理机制,不需要程序员手动释放无用内存。
8、String是最基本的数据类型吗?
Java两种数据类型分类
(1)基本数据类型,分为boolean(1/8)、byte(1)、char(2)、short(2)、int(4)、float(4)、long(8)、double(8); 对应的封装类为Boolean、Byte、Character、Short、Integer、Float、Long、Double。
(2)引用数据类型 ,分为数组、类、接口。
java.lang.String类是由final修饰的,因此String是不可变的。为了提高效率节省空间,我们应该用StringBuffer类
9、int和integer有什么区别
区别:
(1)Integer是int的包装类;int是基本数据类型;
(2)Integer变量必须实例化后才能使用;int变量不需要;
(3)Integer实际是对象的引用,指向此new的Integer对象;int是直接存储数据值
(4)Integer的默认值是null;int的默认值是0。
10、自动装箱和拆箱
自动装箱: 将基本数据类型转化为对象。
自动拆箱: 将对象转化为基本数据类型。
//装箱
Integer num = 10;
//拆箱
int num1 = num;
11、String 、StringBuilder和StringBuffer的区别
String:字符串常量。(String类是由final修饰的)
StringBuffer和StringBuilder:是字符缓冲变量。
StringBuffer和StringBuilder的方法和功能完全是等价的,只是StringBuffer中的方法大都是synchronized修饰的,因此线程安全。而StringBuilder没有这个修饰,因此线程不安全。AbstractStringBuilder是两者的超类(父类)。
-
String类型修饰的字符串是不可变的,StringBuffer和StringBuilder修饰的字符串是可变的。
-
如果是多线程环境下涉及到对象的插入和删除操作,首选StringBuffer;如果是非多线程环境下,首选StringBuilder。
总的来说,三者执行效率的比较:StringBuilder > StringBuffer > String
12、&和&&的区别
&是位运算符,表示位与运算;&&是逻辑与运算符,表示逻辑与(and)。
13、final、fianlly、finalize
final:用于修饰属性、方法和类。分别表示属性不可变、方法不可重写、类不可继承。
finally:是异常处理语句结构的一部分,表示总是执行。
finalize:是Object类的一个方法,是GC进行垃圾回收前要调用的一个方法。(如果实现了非空的这个方法,那么会导致相应对象回收呈现数量级上的变慢,JDK1.9之后就抛弃了)
14、Overload和Override的区别
Overload(重载):是类中方法与方法的多态性。多个同名函数同时存在,但具有不同的参数个数和数据类型,返回的类型可以相同也可以不相同。方法的重载——输入的数据不同,处理也不同(静态多态性)。
Override(重写):是父类与子类的多态性。子类的方法与父类的方法有相同的名称和参数。子类重写父类的方法——相同的参数,不同的实现(动态多态性)。
重写只能重写父类非私有的方法。
重写的规则:
-
参数列表,返回类型必须与被重写方法相同
-
子类重写方法的访问修饰符一定要大于父类被重写的访问修饰符
(public>protected>default>private)
-
子类重写的方法抛出的新检测性异常只能比父类被重写的方法抛出的异常更加宽泛。
重载的规则:
- 不同的参数列表和数据类型。
- 返回类型可以相同或不同,不能以返回类型来判断是否重载。
- 可以抛出不同的异常。
15、抽象类和接口有什么区别
抽象类: 包含抽象方法(只申明,不实现。具体的实现由继承它的子类来实现)的类,即使用abstract修饰的类;不能使用final修饰(final修饰的类不能被继承);抽象类不能被实例化,只能被继承
接口:接口是一个抽象类型,是抽象方法的集合,接口以interface来声明。一个类通过实现接口的方式,从而来覆写接口的抽象方法;接口只能继承接口,不能继承类,接口支持多继承;接口中的定义的成员变量,默认是public static final修饰的静态常量;接口中定义的方法,默认是public abstract修饰的抽象方法
相同点:
① 抽象类和接口都不能被实例化
② 抽象类和接口都可以定义抽象方法,子类/实现类必须覆写这些抽象方法
不同点:
① 抽象类有构造方法,接口没有构造方法
③抽象类可以包含普通方法,接口中只能是public abstract修饰抽象方法(Java8之后可以)
③ 抽象类只能单继承,接口可以多继承
④ 抽象类可以定义各种类型的成员变量,接口中只能是public static final修饰的静态常量
抽象类的使用场景:
既约束子类具有共同的行为,但是不在乎每个实现类如何具体实现。
接口的应用场景:
约束多个实现类具有统一的行为,但是不在乎每个实现类如何具体实现;实现类需要具备很多不同的功能,但各个功能之间可能没有任何联系
16、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);其中有隐含的强制类型转换。
17、Math.round(11.5)等於多少? Math.round(-11.5)等於多少?
11.5+0.5后是12再向下取整是12;-11.5+0.5后是-11再向下取整-11
总的来说:就是参数加个0.5后再向下整。
18、String s = new String(“xyz”);创建了几个String Object?
String s=new String(“xyz”)究竟对象个数分为两种情况:
1.如果String常理池中,已经创建"xyz",则不会继续创建,此时只创建了一个对象new String(“xyz”);
2.如果String常理池中,没有创建"xyz",则会创建两个对象,一个对象的值是"xyz",一个对象new String(“xyz”)。
19、接口是否可继承接口? 抽象类是否可实现(implements)接口? 抽象类是否可继承实体类(concrete class)?
接口可以继承接口。抽象类可以实现接口。抽象类是否可以继承实体类,前提是实体类必须要有明确的构造函数。
20、abstract(抽象类)的方法是否可同时是static,是否可同时是native,是否可同时是synchronized?
- 那么我们就来谈谈这些关键字为什么不能和abstract混用。
- 首先abstract与static,声明static说明可以直接用类名调用该方法;声明abstract说明需要子类重写该方法;如果同时声明static和abstract,用类名调用一个抽象方法肯定不行。
- synchronized 是同步,然而同步是需要有具体操作才能同步的,如果像abstract只有方法声明,那同步一些什么东西就会成为一个问题了,当然抽象方法在被子类继承以后,可以添加同步。
- native本身就和abstract冲突,他们都是方法的声明,只是一个把方法实现移交给子类,另一个是移交给本地操作系统。如果同时出现,就相当于既把实现移交给子类,又把实现移交给本地操作系统,那到底谁来实现具体方法呢!
不能放在一起的修饰符:final和abstract,private和abstract,static和abstract,因为abstract修饰的方法是必须在其子类中实现(覆盖),才能以多态方式调用,以上修饰符在修饰方法时期子类都覆盖不了这个方法,final是不可以覆盖,private是不能够继承到子类,所以也就不能覆盖,static是可以覆盖的,但是在调用时会调用编译时类型的方法,因为调用的是父类的方法,而父类的方法又是抽象的方法,又不能够调用,所以上的修饰符不能放在一起。
21、数组有没有length()这个方法? String有没有length()这个方法?
数组有length属性;String有length()方法。
22、Set里的元素是不能重复的,那么用什么方法来区分重复与否呢? 是用==还是equals()? 它们有何区别?
- Set是Collection容器的一个子接口,允许元素重复,允许有一个null对象
- equals()区分更合适。
== : 判断的两个对象是否同一对象(地址比较)。
equals() : 判断两个对象的内容是否相同,尽量用equals。比较基本类型两者都行。
23、构造器Constructor是否可被Override
构造器不能被重写(Override),但可以被重载(Overload)。
24、swtich是否能作用在byte上,是否能作用在long上,是否能作用在String上?
switch(exp)中,exp是一个整数表达式。switch可以作用于byte、char、short和int上,以及对应的包装类。
25、try {}里有一个return语句,那么紧跟在这个try后的finally {}里的code会不会被执行,什么时候被执行,在return前还是后?
会执行,在return之前执行。
public class TestC {
12
13 @SuppressWarnings("static-access")
14 public static void main(String[] args) {
15 System.out.println("结果: " + new TestC().test()); //(5)
16 }
17
18 static int test(){
19 int i = 1;
20 try {
21 System.out.println("try里面的i : " + i); //(1)
22 return i; (4)
23 }finally{
24 System.out.println("进入finally..."); //(2)
25 ++i; (2)
26 System.out.println("fianlly里面的i : " + i); //(3)
27 }
28 }
29 }
输出结果
try里面的i : 1
进入finally...
fianlly里面的i : 2
结果: 1
在try语句中,在执行return语句时,要返回的结果准备好了,就在此时,程序转到finally执行了,在转去之前,try中先把结果存放到不同于a的局部变量中去,执行完finally之后,在从中去取出返回结果。
因此,即使finally中对变量i进行了改变,但是不会影响结果。
26、编程题: 用最有效率的方法算出2乘以8等於几?
因为将一个数左移n位,就相当于乘以了2的n次方,那么,一个数乘以8只要将其左移3位即可,而位运算cpu直接支持的,效率最高,所以,2乘以8等於几的最效率的方法是2 << 3
-
2*8 2<<3 2左移3位 运算3次
-
8*2 8<<1 8左移1位 运算1次
总的来说:整数乘法或整数除法所需要的时钟周期远远大于移位操作所需的时钟周期
27、两个对象值相同(x.equals(y) == true),但却可有不同的hash code,这句话对不对?
如果判断两个对象相同,不仅要重写equals方法还要重写hashCode方法。
**原因:**如果一个对象没有重写hashCode哈希值默认的是对象的地址,而上面两个对象地址不同
HashSet如何检查重复: 当你把对象插入HashSet时,首先要通过hashCode()方法计算出对象的hashcode值(哈希码)来判断对象加入的位置,同时会与集合中任意一个元素hashCode值是否相等,如果不相等直接将该对象放入集合中。如果hashCode值相等,就会再次通过equals方法判断对象与集合中任何一个对象是否相等,如果不相等,直接将该元素放入集合中,否则不放入。因此,两个对象有相同的hashcode值,但它们不一定相等。如果没有重写hashCode()方法,这两个对象一定不会相等。
总的来说:对象的本质是通过hashCode值判断的,如果让两个不同对象相等,就必须覆写Object的hashCode()和equals()。
hashCode()与equals()的相关规定:
1.如果两个对象相等,这hashCode也一定相同,且两个对象分别调用equals方法都返回true
2.两个对象有相同的hashcode值,它们也不一定是相等的。因此,equals方法被覆盖过,则hashCode方法也必须被覆盖
3.hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则class的两个对象无论如何都不会相等
28、JVM加载class文件的原理机制
类的加载方式分为隐式加载与显式加载两种。
隐式加载指的是程序在使用new等方式生成类或者子类对象,使用类或者子类的静态域是,会隐式地调用类的加载器把对应的类加载到JVM中。
显式加载指的是通过直接调用class.forName()或者ClassLoader.loadClass(className)等方法,显式的把所需要的类加载到JVM中。
29、java中实现多态的机制是什么
多态分为编辑时多态和运行时多态,编辑时多态是静态的,主要指的是重载,根据不同的参数列表区分不同的函数。而其运行时多态是动态的,通过动态绑定来实现的,也就是我们说的多态性。
多态性:父类或接口定义的引用变量可以指向子类或具体实现类的实例对象。提高了程序的拓展性。
在Java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。
多态的实现
Java实现多态有三个必要条件:继承、重写、向上转型。
继承:在多态中必须存在有继承关系的子类和父类。
重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。
只有满足了上述三个条件,我们才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而达到执行不同的行为。
30、java中有几种类型的流?JDK为每种类型的流提供了一些抽象类以供继承,请说出他们分别是哪些类?
字节流,字符流。字节流继承于InputStream OutputStream,字符流继承于InputStreamReader OutputStreamWriter。在java.io包中还有许多其他的流,主要是为了提高性能和使用方便。
31、static关键字和final关键字使用情况;一个类不能被继承,除了final关键字之外,还有什么方法(从构造函数考虑)
static:可以修饰属性、方法,代码块。
修饰属性:相当于一个全局变量,所有对象可以对其修改;随着类的加载而加载(只加载一次),可以使用类名直接调用。
修饰方法:**随着类的加载而加载;可以是用类名直接调用;**静态方法中,只能调用静态的成员,不会出现this;非静态的方法中,可以调用静态和非静态的成员。
static加载顺序问题:静态变量、静态块、构造方法、静态方法、普通方法
final:可以修饰变量、方法、类
修饰变量:修饰的是基本数据类型变量,则初始化之后就不能修改;修饰的引用类型变量,则初始化之后就不能指向其他对象。
修饰方法:表示这个方法不能被重写。
修饰类:表示这个类不能被继承,类中的方法默认为final。
注:private修饰的方法都隐式的指定为final。
JVM
1、JVM的内存区
1、运行过程(Java源文件编译过程)
① Java 源文件—->编译器—->字节码文件
② 字节码文件—->JVM—->机器码
2、JVM内存区域
JVM 内存区域主要分为线程私有区域【程序计数器、虚拟机栈、本地方法区】、线程共享区域【JAVA 堆、方法区】、直接内存。
线程私有数据区域生命周期与线程相同, 依赖用户线程的启动/结束 而 创建/销毁(在 Hotspot VM 内, 每个线程都与操作系统的本地线程直接映射, 因此这部分内存区域的存/否跟随本地线程的生/死对应)。
线程共享区域随虚拟机的启动/关闭而创建/销毁。
直接内存并不是 JVM 运行时数据区的一部分, 但也会被频繁的使用: 在 JDK 1.4 引入的 NIO 提供了基于 Channel 与 Buffer 的 IO 方式, 它可以使用 Native 函数库直接分配堆外内存, 然后使用DirectByteBuffer 对象作为这块内存的引用进行操作(详见: Java I/O 扩展), 这样就避免了在 Java堆和 Native 堆中来回复制数据, 因此在一些场景中可以显著提高性能。
堆(Heap)
创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。由于现代VM采用分代收集算法,因此Java堆从GC的角度还可以细分为:新生代(Eden 区、From Survivor 区和 To Survivor 区)和老年代。
方法区(Method Arear)
用于存储被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。Jdk1.8之前称之为永久代,Jdk1.8以后称之为元空间。
运行时常量池(Runtime Constant Pool):用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
虚拟机栈(VM Stack)
是描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、返回地址等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
栈帧( Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)、 方法返回值和异常分派( Dispatch Exception)。栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。
在Java虚拟机规范中,对此区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出StackOverflowError异常;如果虚拟机栈动态扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
本地方法栈(Native Method Stack):本地方法栈和虚拟机栈类似,不同的是虚拟机栈是Java方法服务,而本地方法栈是Native方法服务。在HotSpot虚拟机实现中是把本地方法栈和虚拟机栈合二为一的,同理它也会抛出StackOverflowError和OOM异常。
PC程序计数器(Program Counter Register)
它是一块较小的内存空间。是当前线程所有执行的字节码的行号指示器。正在执行 java 方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。如果还是 Native 方法,则为空。
这个内存区域是唯一一个在虚拟机中没有规定任何OutOfMemoryError 情况的区域。
2、JVM运行时内存
Java 堆从 GC 的角度还可以细分为: 新生代(Eden 区、ServivorFrom 区和 SurvivorTo区)和老年代。
新生代
是用来存放新生的对象。一般占据堆的 1/3 空间。由于频繁创建对象,所以新生代会频繁触发MinorGC 进行垃圾回收。新生代又分为 Eden、ServivorFrom、ServivorTo 三个区。
- Eden区:**Java 新对象的出生地(**如果新创建的对象占用内存很大,则直接分配到老
年代)。当 Eden 区内存不够的时候就会触发 MinorGC,对新生代区进行
一次垃圾回收。 - ServivorFrom:上一次 GC 的幸存者,作为这一次 GC 的被扫描者。
- ServivorTo:保留了一次 MinorGC 过程中的幸存者。
MinorGC的过程(复制->清空->互换)
Minor(新生代)采用的是复制算法
1:Eden、ServivorFrom复制到ServivorTo,年龄+1
首先,把 Eden 和 ServivorFrom 区域中存活的对象复制到 ServicorTo 区域(默认情况下年龄到达 15 的对象会被移到老生代中),同时把这些对象的年龄+1(如果 ServicorTo 不够位置了就放到老年区);
2:清空Eden、ServivorFrom中的对象;
然后,清空 Eden 和 ServicorFrom 中的对象;
3:ServivorTo和ServivorFrom互换
最后,ServicorTo 和 ServicorFrom 互换,原 ServicorTo 成为下一次 GC 时的 ServicorFrom区。
老年代
主要存放应用程序中生命周期长的内存对象。
老年代的对象比较稳定,所以 MajorGC 不会频繁执行。在进行 MajorGC 前一般都先进行了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。
MajorGC 采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。ManorGC 的耗时比较长,因为要扫描再回收。ManorGC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出 OOM(Out Of Memory)异常。
永久代
指内存的永久保存区域,主要存放 Class 和 Meta(元数据)的信息,Class 在被加载的时候被放入永久区域,它和和存放实例的区域不同,GC 不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异常。
在Java8之后,永久代已被“元数据区”所取代。元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory, 字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由MaxPermSize 控制, 而由系统的实际可用空间来控制。
2、Java 的引用类型中:强引用、软引用、弱引用、虚引用的区别
强引用: 在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。例如Object b = new Object()。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。
软引用: 弱于强引用,软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。
弱引用: 弱于软引用,弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。
虚引用: 最弱的引用,虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。
3、垃圾回收与算法
(1)如何确定垃圾
-
引用计数法
**通过引用计数来判断一个对象是否可以回收。**间单说,给对象中添加一个引用计数器,每当有一个地方引用它,计数器值加1,每当有一个引用失效时,计数器值减1.任何时刻计数器值为零的对象就是不可能再被使用的,那么这个对象就是可回收对象。
-
可达性分析
Java 使用了可达性分析的方法。通过一系列的“GC roots”对象作为起点搜索。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。
要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。
(2)垃圾回收算法
1、标记-清除算法
算法分为“标记”和“清除”阶段:标记阶段标出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。它是最基础的垃圾收集算法。后续的算法都是对其不⾜进⾏改进得到。缺陷:内存碎片化严重、内存效率低。
2、复制算法(大部分的JVM的GC对于新生代都采用复制算法)
为了解决内存碎片化问题,“复制”收集算法出现了。它可以将内存分为⼤⼩相同的两块,每次使⽤其中的⼀块。当这⼀块的内存使⽤完后,就将还存活的对象复制到另⼀块去,然后再把使⽤的空间⼀次清理掉。这样就使每次的内存回收都是对内存区间的⼀半进⾏回收。优点:内存效率高,不易产生碎片。缺陷:存活对象多,效率就会大大减低
3、标记-整理算法(老年代回收的对象很少采用标记整理算法)
根据⽼年代的特点(回收少量对象)出的⼀种标记算法,标记过程仍然与“标记-清除”算法⼀样,但后续步骤不是直接对可回收对象回收,⽽是让所有存活的对象向⼀端移动,然后直接清理掉端边界以外的内存。
4、分代收集算法
当前虚拟机的垃圾收集都采⽤分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存划分不同的域。⼀般将java堆分为新⽣代和⽼年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
⽐如在新⽣代中,每次收集都会有⼤量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。⽽⽼年代的对象存活机率是⽐较⾼的,⽽且没有额外的空间对它进⾏分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进⾏垃圾收集。
4、常见的垃圾收集器有哪些
新生代GC分为Serial、ParNew、
Serial(单线程+复制算法)
最基础的收集器,是单线程收集器,使用复制算法,新生代唯一的垃圾收集器。只会使用一个CPU或一条线程取完成垃圾收集工作,同时必须要暂停其他所有的工作线程,直到线程结束。
Serial 垃圾收集器虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效,对于CPU环境来说,Serial 由于没有线程交互开销,可获得最高的单线程收集效率。因此,Serial是虚拟机运行在Client模式下默认的新生代垃圾收集器。
ParNew(Serial+多线程)
ParNew是多线程收集器,使用复制算法,除了使用多线程进行垃圾收集外其余行为和Serial完全一致。ParNew 是虚拟机运行在Server模式下默认的新生代收集器,一个重要原因是除了 Serial 外只有它能与 CMS配合。自从 JDK 9 开始,ParNew 加 CMS 不再是官方推荐的解决方案,官方希望它被 G1 取代。
Parallel Scavenge(ParNew+可控制吞吐量、高效)
Parallel Scavenge是多线程收集器,使用复制算法,与 ParNew 类似。特点是它的关注点与其他收集器不同,Parallel Scavenge 的目标是达到一个可控制的吞吐量,吞吐量=运行用户代码的时间/处理器消耗总时间。可以最高效的利用CPU时间,尽快地完成程序的运行任务,主要不需要太多的交互任务,自适应调节策略也是 ParallelScavenge 收集器与 ParNew 收集器的一个重要区别。
注:ParNew和Parallel Scavenge在收集垃圾过程中都需要暂停所有的线程工作。
Serial Old(单线程标记整理算法)
Serial 的老年代版本,单线程工作,使用标记-整理算法。
Serial Old 是虚拟机在Client模式下的默认老年代收集器,Server模式下有两种用途:
① JDK1.5 之前与Parallel Scavenge 搭配使用。
② 作为老年代中使用CMS 收集器的后备垃圾收集方案。
图:新生代Serial与老年代Serial Old搭配垃圾收集过程
图:Scavenge/ParNew与老年代Serial Old搭配垃圾收集过程
Parellel Old(多线程标记整理算法)
是Parallel Scavenge 的老年代版本,使用多线程的标记-整理算法。JDK1.6 之前,Parallel Old 正是为了在年老代同样提供吞吐量优先的垃圾收集器,要求吞吐量比较高可考虑Parallel Scavenge 加 Parallel Old。
图:Parrallel Scavenge加Parallel Old收集器运行过程
CMS(多线程标记清除算法) 主要目的是获取最短回收停顿时间,是多线程的标记-清除算法,最短回收停顿时间可以为交互比较高的程序提高用户体验,过程相对比其他收集器复杂,分为四个步骤:初始标记、并发标记、重新标记、并发清除。
初始标记:只是标记一下GC Root能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
并发标记:进行GC Root跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
重新标记:为了修正在并发标记期间,因用户继续运行而导致标记产生变动的那一步对象的标记记录,仍然需要暂停所有的工作线程。
并发清除:清除GC Root不可达对象,和用户线程一起工作,不需要暂停工作线程。由于并发标记和并发清除过程中,垃圾回收线程可以和用户在一起并发工作,所以总体来看CMS收集器的内存回收和用户线程是一起并发的执行。
图:CMS收集工作过程
缺点:
① 对处理器资源敏感,并发阶段虽然不会导致用户线程暂停,但会降低吞吐量。
② 无法处理浮动垃圾,有可能出现并发失败而导致 Full GC。
③ 基于标记-清除算法,产生内存碎片。
G1收集器
主要面向服务端,最初设计目标是替换 CMS,相比CMS收集器,G1两个最突出的改进是:
1. 基于标记-整理算法,不产生内存碎片。
2. 可以非常精确的控制停顿时间,在不牺牲吞吐量的前提下,实现低停顿垃圾回收。
G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率。
G1 运作过程:
初始标记: 标记 GC Roots 能直接关联到的对象,让下一阶段用户线程并发运行时能正确地在可用Region 中分配新对象。需要 STW 但耗时很短,在 Minor GC 时同步完成。
并发标记: 从 GC Roots 开始对堆中对象进行可达性分析,递归扫描整个堆的对象图。耗时长但可与用户线程并发,扫描完成后要重新处理 SATB 记录的在并发时有变动的对象。
最终标记: 对用户线程做短暂暂停,处理并发阶段结束后仍遗留下来的少量 SATB 记录。
筛选回收: 对各 Region 的回收价值排序,根据用户期望停顿时间制定回收计划。必须暂停用户线程,由多条收集线程并行完成。
可由用户指定期望停顿时间是 G1 的一个强大功能,但该值不能设得太低,一般设置为100~300 ms。
容器
1、java 容器都有哪些?
接口继承关系和实现:
集合分为Iterator、Collection、Map。
- Collection:Collection是集合List、Set、Queue的最基础的接口。
- Iterator:迭代器,可以通过迭代器遍历集合中的数据
- Map:是映射表的基础接口
主要实现类:
- List: Vector、ArrayList、LinkedList
- Set: HashSet、TreeSet
- Map: HashMap、TreeMap、HashTable
2、Collection 和 Collections 有什么区别?
Collection是集合类的接口,子类接口主要有List和Set
Collections是集合的一个工具类,他提供一系列静态方法实现对各种集合的查找、排序、线程安全等操作。
3、List、Set、Map 之间的区别是什么?
由Collection接口派生的两个接口LIst和Set。
- List(列表)是有序、可重复的集合;Set(集)是无序、不可重复的集合。
- Set:查找元素效率低,删除和插入的效率高,不会引起元素的位置变化。
- List:查找元素效率高,删除和插入的效率低,会引起其他元素的发生变化。
- Map(映射)是独立的接口,是一组key到value进行映射的集合。key是不能重复的(唯一),它的value是可以重复的。
4、HashMap 和 Hashtable 有什么区别?
1、线程安全性和效率性
HashMap是线程不安全的,所以效率高;但可以通过Collection工具类来得到线程安全的类;Hashtable是线程安全的,是通过Synchronized 实现的,所以效率低。
2、是否允许null
HashMap是允许key、value为null的;Hashtable不允许key、value为null的,否则会抛出异常。
3、扩容方式不同(容量不够)
1. capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍。
2. loadFactor:负载因子,默认为 0.75。
3. threshold:扩容的阈值,等于 capacity * loadFactor
4. size:它记录HashMap的底层数组中已用槽的数量
HashMap扩容为容量的2^n倍,Hashtable为原容量的2^n+1倍。
4、解决冲突的方法不同
冲突容量大于8时,就会将冲突的entry数组转换为红黑树进行存储;
冲突容量小于6时,又会装换为链表存储。
Hashtable都是以链表的方式存储。
5、如何决定使用 HashMap 还是 TreeMap?
区别:
TreeMap
-
TreeMap<K, V>的Key值要求实现Comparable接口,或者构造TreeMap传入自定义的Comparable,TreeMap默认是按照Key值升序排列的;
-
TreeMap是基于红黑树结构的实现,适用于自然排序或自定义顺序遍历key。
HashMap
- HashMap<K, V>的值实现hashCode(),分布均匀的,不支持排序。
- HashMap是基于桶(数组)+链表+红黑树实现,适用于插入,删除和定位元素。
6、说一下 HashMap 的实现原理?
- 底层结构
1、HashMap实现了Map*接口,JDK7底层通过数组+链表实现,时间复杂度为O(N)。JDK8底层通时间过数组+链表+红黑树实现,时间复杂度为O(logN)。是无序的,key和value都允许插入null元素。支持快速访问和元素存储。数组进行扩容时,数组默认初始大小为16,负载因子0.75,扩容阈值:初始容量*负载因子,扩大为其容量的2倍。链表元素个数为8且数组大小为64时,链表自动转换为红黑树。链表长度小于6时,红黑树又自动转换成链表。(HashMap由数组(键值对entry组成的数组主干)+ 链表(元素太多时为解决哈希冲突数组的一个元素上多个entry组成的链表)+ 红黑树(当链表的元素个数达到8链表存储改为红黑树存储)进行数据的存储)。
2、HashMap不是线程安全的,效率比较高,只能用于单线程的环境中,在多线程环境中可以使用Collections.syschronizedList(Map map)函数返回一个线程安全的HashMap集合,或者使用JUC并发包下的ConcurrentHashMap,或者是Hashtable
Map<String, Object> map = new Has htable<>(); //synchronized修饰get/put方法。方法级阻塞,只能同时一个线程操作get或put
Map<String, Object> map =Collections.synchronizedMap(new HashMap<String, Object>()); //所有方法都使用synchronized修饰
Map<String, Object> map = new ConcurrentHashMap<>(); //每次只给一个桶(数组项)加锁。
- hash冲突
hashCode()方法决定了对象会被放到哪个位置,当多个对象的哈希值冲突时,equals()方法决定了这些对象是否是同一个对象;每次都需要@Override复写hashCode()和equals()方法
7、ConcurrentHashMap的理解
Segment段
ConcurrentHashMap 和 HashMap 思路是差不多的,但是因为它支持并发操作,所以要复杂一些。整个 ConcurrentHashMap 由一个个 Segment 组成,Segment 代表”部分“或”一段“的意思,所以很多地方都会将其描述为分段锁。注意,行文中,我很多地方用了“槽”来代表一个segment
线程安全
ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承ReentrantLock(重进入锁)来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。
Java7ConcurrentHashMap结构:Segment数组+HashEntry数组+链表
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZWWVAYld-1599727491311)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20200906101845023.png)]
并行度(默认16)
concurrencyLevel:并行级别、并发数、Segment 数,怎么翻译不重要,理解它。默认是 16,也就是说 ConcurrentHashMap 有 16 个 Segments,所以理论上,这个时候,最多可以同时支持 16 个线程并发写,只要它们的操作分别分布在不同的 Segment 上。这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩容的。再具体到每个 Segment 内部,其实每个 Segment 很像之前介绍的 HashMap,不过它要保证线程安全,所以处理起来要麻烦些
Java8ConcurrentHashMap结构:Segment数组+HashEntry数组+链表+红黑树
7、ArrayList的理解
-
底层结构 (实现了什么接口,底层通过什么实现,是什么容器,是否允许存放null元素)
1、ArrayList实现了List接口**,底层通过数组实现,是有序的,允许插入null元素,支持快速随机访问和元素重复。数组进行扩容时,数组默认初始大小为10,扩大为其原容量的1.5倍。时间复杂度为O(N)
2、ArrayList不是线程安全的,效率比较高,只能用于单线程的环境中,在多线程环境中可以使用Collections.synchronizedList(List list)函数返回一个线程安全的ArrayList集合,或者使用JUC并发包下的CopyOnWriteArrayList(CopyOnWriteArrayList使用了一种叫写时复制的方法,当有新元素添加到CopyOnWriteArrayList时,先从原有的数组中拷贝一份出来,然后在新的数组做写操作,写完之后,再将原来的数组引用指向到新数组。)
实现线程安全:原生的Vector;Collections.syschronized()是通过Syschronized实现线程同步;CopyOnWriteArrayList()是通过ReenTrantLock保证线程可见性和顺序性
List<Integer> map =Collections.synchronizedList(new ArrayList<Integer>());
ArrayList和LinkList 的区别:
LinkedList底层基于双向链表实现,ArrayList底层基于数组实现。都是线程不安全的。LinkedList适用于插入、删除,ArrayList适用于查找。专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。
异常
1、Throw和throws的区别
位置不同:
throws 用在函数上,后面跟的是异常类,可以跟多个。throw 用在函数内,后面跟的是异常对象。
功能不同:
1、throws 用来声明异常,让调用者只知道该功能可能出现的问题,可以给出预先的处理方式;throw抛出具体的问题对象,执行到throw,功能就已经结束了,跳转到调用者,并将具体的问题对象抛给调用者。也就是说 throw 语句独立存在时,下面不要定义其他语句,因为执行不到。
2、throws 表示出现异常的一种可能性,并不一定会发生这些异常;throw 则是抛出了异常,执行 throw 则一定抛出了某种异常对象。
3、两者都是消极处理异常的方式,只是抛出或者可能抛出异常,但是不会由函数去处理异常,真正的处理异常由函数的上层调用处理。
2、异常体系
Throwable是Error和Exception的超类(父类)。
Error: 是指Java运行时系统内部错误和资源耗尽错误,并不会抛出该类异常。
Exception: 分为IOException和RuntimeEception两大类。
RuntimeException运行时异常
如 NullPointException指针异常、ClassException类异常;
RuntimeExcption是哪些可能在Java虚拟机正常运行期间抛出的异常的超类,这些异常一般是由程序逻辑错误引起的,程序员应该从逻辑角度尽可能避免这类异常的发生。
CheckedException检测时异常
如I/O错误导致的IOException输入输出异常、SQLException数据库异常;
CheckedException:一般是外部错误,这种异常都发生在编译阶段,Java编译器会强制程序去捕获此类异常,即会出现要求你把这段可能出现异常的进行try……catch,这类异常一般包括几个方面:
- 试图在文件尾部读取数据
- 试图打开一个错误格式的URL
- 试图根据给定的字符串找class对象,而这字符串表示的类并不存在
反射
1、谈谈反射的理解
反射机制:动态获取信息和动态调用对象方法的功能。java反射机制在运行状态中,对于任意一个类都能知道这个类的所有的属性和方法;并且对于任意一个对象,都能调用它的任意一个方法。(运行状态中知道类所有的属性和方法)
应用场合:JDBC驱动加载、spring框架配置文件读取等。
2、Java反射API
反射API用来生成JVM中的类、接口或对象的信息。
- Class类:反射的核心类,可以获取类的属性和方法等信息。
- Field类:Java.lang.reflec包中,表示类成员变量,可以获取和设置类中的属性值。
- Method类:Java.lang.reflec包中类,表示类方法,它可以获取类中的执行信息和方法信息。
- Constructor类:Java.lang.reflec包中的类,表示类的构造方法。
3、反射使用步骤(获取Class对象、调用对象方法)
- 获取class对象(核心),通过Class对象可以调用类的方法。
- 调用的Class对象,即是反射的使用阶段。
- 使用反射API来操作这些信息。
4、获取Class对象的3种方法
-
调用对象的getClass()方法
Person p = new Person(); Class c = p.getClass();
-
调用类的class属性
Class c = Person.class;
-
使用Class类的forName()方法(最安全、性能最高)
Class c = Class.forName(“类的全部路径”);
//(最常用)```
5、创建对象的两种方法
Class对象的newInstance()
- 使用Class对象的newInstance()方法来创建Class对象对应类的实例,但是这种方法要求该Class对象对应的类有默认的空构造器。
调用Constructor对象的newInstance()
-
先使用Class对象获取指定的Constructor对象,在调用Constractor对象的newInstance()方法来创建Class对象对应类的实例,通过这种方法创建实例。
Class c = Class.forName("reflection.Person"); //使用.newInstance方法创建对象 Person p = (Person) c.newInstance(); //获取构造方法并创建对象 Constructor c1 = c.getDeclaredConstructor(String.class, String.class, int.class); //创建对象并设置属性 Constructor p1 = (Person) c.newInstance("李四", "男", 20);
6、深拷贝 VS 浅拷贝
浅拷贝: 对基本数据类型进行传值,对引用数据类型进行引用传递般的拷贝。
深拷贝: 对基本数据类型进行传递,对引用数据类型,先创建一个对象再复制其内容。
7、多线程
1、为什么需要多线程
1)CPU增加了缓存,以均衡与内存的速度差异。//导致可见性问题
2)操作系统增加了进程、线程,以分时复用CPU,以均衡CPU与I/O设备的速度差异 //导致原子性问题
3)编译程序优化指令执行顺序,使得缓存能够合理的利用 //导致有序性问题
2、并发三要素
1、可见性:CPU缓存引起
可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到。
//线程1执行的代码
int i = 0;
i = 10;
List item
//线程2执行的代码
j = i;
2、原子性:分时复用引起
原子性:一个或者多个操作全部执行并且执行的过程不会被任何因素打断,或者都不执行。
int i = 1;
// 线程1执行
i += 1;
// 线程2执行
i += 1;
3、有序性:重排序引起
有序性:程序执行代码的顺序按照代码的先后顺序执行。
Java是怎么解决并发问题:JMM(内存模型)
JMM本质:Java内存模型规范了JVM如何提供按需禁用缓存和编译优化的方法。
- volatile、synchronized和final三个关键字
- Happens-Before规则
理解第二个维度:可见性、有序性、原子性
- 原子性:对基本数据类型变量读取和赋值是原子性操作。(可以通过synchronized和Lock保证任一时刻只有一个线程执行该代码块)
- 可见性:Java提供了volatile关键字来保证可见性。(可以通过volatile修饰的变量,会保证修改的值立即被更新到主存,线程需要读取时,会直接到内存中读取)
- 有序性:volatile可以保证一定的“有序性”,synchronized和Lock保证每个时刻是有一个线程同步代码块。JMM是通过Happens-Before规则保证有序性的。
Happens-Before规则
1、单一线程原则
在一个线程内,在程序前面的操作优先于后面的操作之前发生
2、管程锁定规则
一个unlcok操作优先于后面对同一个锁的Lock操作
3、volatile变量规则
对一个volatile变量的写操作优先于后面这个变量的读操作
4、线程启动规则
Thread对象的start()方法调用优先于此线程的每一个动作。
5、线程加入规则
Thread对象的结果优先于join()方法返回
6、线程中断规则
线程interrupt()方法的调用优先于被中断线程的diamante检测到中断时间的发生,可以通过interrupted()方法检测到是否又中断发生
7、对象终结规则
一个对象的初始化完成(构造函数执行结束)优先于它的finalize()方法的开始
8、传递性
如果操作A优先于操作B,操作B优先于操作C,那么操作A优先于操作C
线程的安全实现方法
1、互斥同步(阻塞同步)
synchronized和ReetrantLock
互斥同步最主要的问题就是线程阻塞和唤醒所带来的的性能问题,因此这种同步也称为阻塞同步,属于一种悲观的并发策略。
2、非阻塞同步
先进行操作,如果没有其他线程争用共享数据,那操作就成功了,否则采取补偿措施(不断的重试,直到成功为止),属于一种乐观的并发策略。
- CAS(Compare-and-Swap,比较并交换)
CAS指令需要有三个操作数,分别是内存地址V,旧的预期值A和新值B。当执行操作是,只有当V的值等于A,才将V的值更新为B. - AtomicInteger
J.U.C包里面的整数原子类AtomicInteger,其中的compareAndSet()和getAndIncrement()等方法都是用了Unsafe类的CAS操作。
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!thispareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
getAndAddInt()源码,var1指示对象内存地址,var2指示该字段相对对象内存地址的偏移,var4指示操作需要加的数值(这里为1),如果该字段内存地址中的值等于var5,那么就更新内存地址为var1+var2的变量为var5+var4.
可以看到 getAndAddInt() 在一个循环中进行,发生冲突的做法是不断的进行重试。
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!thispareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
- ABA
如果一个变量读取的时候是A值,它的值被改成了B,后来又被改回A,那么CAS操作就会误认为它从来没有被改变过。
3、无同步方案
如果一个方法不涉及共享数据,那它自然就无需任何同步措施去保证正确性。
(1)栈封闭
多个线程访问同一个方法的局部变量是,不会出现安全问题,英文局部变量存储在虚拟机栈中,属于线程私有的。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class StackClosedExample {
public void add100() {
int cnt = 0;
for (int i = 0; i < 100; i++) {
cnt++;
}
System.out.println(cnt);
}
}
public static void main(String[] args) {
StackClosedExample example = new StackClosedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> example.add100());
executorService.execute(() -> example.add100());
executorService.shutdown();
}
输出结果:
100
100
(二)线程本地栈存储
通过java.lang.ThreadLocal类实现本地存储功能。
(三)可重入代码
可以再代码中执行的任何时刻终端他,转而执行另外一段代码。
线程状态转换
1、新建(New)
Thread thread = new Thread();
2、可运行(Runnable)
Thread.start();
3、阻塞(Blocking)
syschronized或者Lock锁顺序执行代码
4、无限期等待(Waiting)
进入无限期等待方法
进入方法 | 退出方法 |
---|---|
没有设置Timeout参数的Thread.wait()方法 | Object.notify()或者是Object.notifyAll() |
没有设置Timeout参数的Thread.join()方法 | 被调用的线程执行完毕 |
LockSupport.park()方法 |
Object.wait()或者Object.notify()
5、限期等待(Timed Waiting)
Thread.sleep(1000); //等待一秒
进入方法 | 退出方法 |
---|---|
Thread.sleep()方法 | 时间结束 |
设置了TimeOut参数的Object.wait()方法 | 时间结束/Objectd.notify()或者是Object.notifyAll() |
设置了Timeout参数的Thread.join()方法 | 时间结束/被调用的线程执行完毕 |
LockSupport.parkNanos()方法 | - |
LockSupport.parkUntil()方法 | - |
6、死亡(Terminated)
线程结束或者是产生异常结束
线程使用方法
- 实现Runnable接口
- 实现Callable接口
- 继承Thread类
实现Runnable接口
实现run()方法,通过Thread调用start()来启动线程
class MyRunnable implements Runnable{ //实现Runnable接口
@Override
public void run() { //覆写run()方法
System.out.println("线程1");
}
}
public class RunnableExample {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable(); //初始化MyRunnable类
Thread thread = new Thread(myRunnable); //通过Runnable类初始化Thread类
thread.start(); //启动线程
}
}
输出结果
线程1
实现Callable接口
Callable有返回值,实现call方法,返回值通过FutureTask进行封装。
class MyCallable implements Callable{ //实现Callable接口
@Override
public Integer call(){ //覆写call方法
System.out.println("线程1");
return 123;
}
}
public class CallableExample {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable myCallable = new MyCallable(); //初始化MyCallable类
FutureTask futureTask = new FutureTask(myCallable); //返回值通过FutureTask进行封装
Thread thread = new Thread(futureTask); //通过FutureTask初始化Thread类
thread.start(); //启动线程
System.out.println(futureTask.get()); //获取返回值
}
}
输出结果
线程1
123
继承Thread类
Thread类实现了Runnable接口
class MyThread extends Thread{ //继承Thread类,Thread类实现了Runnable接口,覆写了run()方法
@Override
public void run() {
System.out.println("线程1");
}
}
public class ExtendsThreadExample {
public static void main(String[] args) {
MyThread thread = new MyThread(); //初始化MyThread类
thread.start(); //启动线程
}
}
输出结果
线程1
实现接口 VS 继承Thread
实现接口更好。
- Java不支持多继承,当时可以实现多个接口
- 继承Thread类开销过大。
基础线程机制
Executor
Executor管理多个异步任务的执行,异步指的是多个任务的执行互不打扰,不需要同步进行。
主要有三种异步操作:
- CacheThreadPool:一个任务创建一个线程
- FixedThreadPool:所有任务只能使用固定大小的线程
- SingleThreadExecutor:相当于大小为1的FixedThreadPool
public class ExecutorExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool(); //通过Executors类调用newCachedThreadPool()静态方法,返回一个ExecutorService接口
for(int i = 0; i < 5; i ++) {
executorService.execute(new MyRunnable()); //executorService继承了Executor接口,所以实现了execute()方法
}
executorService.shutdown(); //关闭异步
}
}
输出结果
线程1
线程1
线程1
线程1
线程1
Daemon
Daemon称为守护线程,是程序运行时在后台提供服务的线程。
使用setDaemon(true)设置为守护线程。
pucli static void main() {
Thread thread = new Thread(); //初始化线程
thread.setDaemon(true); //设置thread线程为守护线程
}
sleep()
Thread.sleep(millisec)方法会休眠当前正在执行的线程,millisec单位为毫秒。
public void run() {
try{
Thread.sleep(1000); //设置当前线程休眠1秒
}catch (InterruptedException e) {
e.printStackTrace();
}
}
yield()
Thread.yield()方法声明了当前线程已经完成了生命周期中最重要的部分。可以切换其他线程来执行。
public void run() {
Thread.yield(); //当前线程已完成生命周期中最重要的部分
}
线程中断
线程结束或者产生异常提前结束
InterruptedException
interrupt()方法用来中断该线程的,如果线程处于阻塞、限期等待、无限期等待状态,就会抛出InterruptedException异常,从而提前结束,但是不能终端I/O阻塞和syschronized锁阻塞。
通俗一点就是:interrupt()是中断处于阻塞、限期等待、无限期等待状态下的线程
class MyThread01 extends Thread{
@Override
public void run() {
try{
Thread.sleep(1000);
System.out.println("Thread run");
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class InterruptExample {
public static void main(String[] args) {
MyThread01 myThread = new MyThread01();
myThread.start();
myThread.interrupt();
System.out.println("main run");
}
}
输出结果
Main run
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at InterruptExample.lambda$main$0(InterruptExample.java:5)
at InterruptExample$$Lambda$1/713338599.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
interrupted()
interrupted()用来判断线程是否处于中断状态。
class MyRunnable01 implements Runnable{ //实现Runnable接口
@Override
public void run() { //覆写run()方法
while(interrupted()) {
System.out.println("线程1"); //方法体
}
System.out.println("Thread end"); //方法体
}
}
public class InterruptedExample {
public static void main(String[] args) {
MyRunnable01 myRunnable = new MyRunnable01();
Thread thread = new Thread(myRunnable);
thread.start();
thread.interrupt();
}
}
输出结果
Thread run
Executor的中断操作
shutdown()方法会等待所有代码执行完才关闭线程。
shutdownNow()方法相当于调用了interrupt() 方法。
public class ExecutorShotdownExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> {
try{
Thread.sleep(2000);
System.out.println("Thread run");
}catch (InterruptedException e) {
e.printStackTrace();
}
});
executorService.shutdownNow(); //相当于interrupt()方法
System.out.println("Main run");
}
}
输出结果
Main run
线程互斥同步
Java提供了两种多线程控制访问,第一种是JVM提供的synchronized,第二种是JDK提供的ReentrantLock
synchronized
互斥同步指的是同一个时间只有一个线程能够访问同步代码,其他的线程等待。
1、同步代码块
作用于同一个对象,两个不同对象的代码块,不会进行同步
class SynchronizedBlock{
public void func1() {
synchronized (this) {
for(int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
}
}
}
public class SynchronizedBlockExample {
public static void main(String[] args) {
SynchronizedBlock e1 = new SynchronizedBlock(); //初始化SynchronizedBlock类
SynchronizedBlock e2 = new SynchronizedBlock();
ExecutorService executorService = Executors.newCachedThreadPool(); //创建一个新缓存线程池
//不同对象调用代码块,是异步进行的
executorService.execute(() -> e1.func1());
executorService.execute(() -> e2.func1());
}
}
输出结果
0 1 2 3 4 5 6 7 8 0 1 2 9 3 4 5 6 7 8 9
public class SynchronizedBlockExample {
public static void main(String[] args) {
SynchronizedBlock e1 = new SynchronizedBlock(); //初始化SynchronizedBlock类
ExecutorService executorService = Executors.newCachedThreadPool(); //创建一个新缓存线程池
//同一个对象调用代码块互斥同步
executorService.execute(() -> e1.func1());
executorService.execute(() -> e1.func1());
}
}
输出结果
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
2、同步方法
和同步代码块一样,也是作用于同一个对象
class SynchronizedMethod{
public synchronized void func1() {
for(int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
}
}
3、同步类
作用于整个类,多个线程调用一个类的同一对象或者是不同对象也会同步
class SynchronizedClass{
public void func1() {
synchronized (SynchronizedClass.class) {
for(int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
}
}
}
public class SynchronizedBlockExample {
public static void main(String[] args) {
SynchronizedClass e1 = new SynchronizedClass();
SynchronizedClass e2 = new SynchronizedClass();
ExecutorService executorService = Executors.newCachedThreadPool(); //创建一个新缓存线程池
executorService.execute(() -> e1.func1());
executorService.execute(() -> e2.func1());
}
}
输出结果:
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
4、同步静态方法
和同步类一样,作用于整个类,
class SynchronizedClass{
public synchronized static void func2() {
for(int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
}
}
synchronized VS ReentrantLock
1、锁的实现:synchronized是JVM实现的,ReentrantLock是JDK实现的
2、性能:新版本Java对synchronized进行了很多优化,例如自旋锁等,其他的大致相同。
3、等待可中断:当持有锁的线程长期不释放,等待中的线程可以选择放弃等待,改为处理其他代码。ReentrantLock可中断,synchronized不行。
4、公平锁:多个线程等待一个锁时,必须按照申请锁的时间循序来依次获的锁。synchronized是非公平的,ReentrantLock默认情况下是非公平的,但也可以是公平的。
5、锁绑定多个条件:一个ReentrantLock锁可以同时绑定多个Condition对象。
join()
在线程中调用另外一个线程join()方法,会将当前线程挂起,而不是等待,知道目标线程结束。
public class JoinExample {
public static void main(String[] args) {
JoinExample example = new JoinExample();
A a = new A();
B b = new B(a);
b.start();
a.start();
}
}
class A extends Thread{
@Override
public void run() {
System.out.println("A.run");
}
}
class B extends Thread{
private A a;
B(A a) {
this.a = a;
}
@Override
public void run() {
try {
a.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("B.run");
}
}
输出结果
A.run
B.run
wait()、notify()、notifyAll()
wait()、notify()、notifyAll()都是Object方法,并且只能在同步方法或者同步控制块中使用,否者会抛出IllegalMonitorStateException异常。
wait()是用来使当前线程挂起,notify()只能唤醒一个线程,notifyAll()可以唤醒全部线程
public class WaitNotifyExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
WaitNotifyExample waitNotifyExample = new WaitNotifyExample();
executorService.execute(() -> waitNotifyExample.after());
executorService.execute(() -> waitNotifyExample.before());
}
public synchronized void before() {
System.out.println("WaitNotifyExample.before");
notify();
}
public synchronized void after() {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("WaitNotifyExample.after");
}
}
输出结果
wait()、sleep()
- wait() 是Object方法、sleep()是Thread静态方法
- wait() 会释放锁,sleep()不会
await()、signal()、signalAll()
Condition接口实现了await()、signal()、signalAll()方法。
await()是用来使当前线程挂起,signal()只能唤醒一个线程,signalAll()可以唤醒全部线程
public class AwaitSignalExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
AwaitSignalExample example = new AwaitSignalExample();
executorService.execute(() -> example.before()); //执行线程并唤醒全部线程
executorService.execute(() -> example.after()); //线程一挂起
executorService.execute(() -> example.after()); //线程二挂起
executorService.execute(() -> example.before()); //执行线程并唤醒全部线程
}
private Lock lock = new ReentrantLock(); //初始化ReentrantLock非公平锁
private Condition condition = lock.newCondition(); // 使用 Lock 来获取一个 Condition 对象
private void before() {
lock.lock(); //释放锁
try{
System.out.println("AwaitSignalExample.before");
condition.signalAll(); //唤醒全部线程
}finally {
lock.unlock(); //关闭锁
}
}
private void after() {
lock.lock();
try{
condition.await(); //当前线程挂起
System.out.println("AwaitSignalExample.after");
}catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
输出结果
AwaitSignalExample.before
AwaitSignalExample.before
AwaitSignalExample.after
AwaitSignalExample.after
Synchronized的使用
- 代码块形式:synchronized手动指定锁定对象,可以是this,也可以是自定义的锁
public class SynchronizedObjectLock implements Runnable{
static SynchronizedObjectLock instance = new SynchronizedObjectLock();
@Override
public void run() {
// 同步代码块形式--锁为this,两个线程使用的锁时一样的,线程1必须要等到线程0释放了该锁后,才能执行。
synchronized (this) {
System.out.println("我是线程" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "结束");
}
}
public static void main(String[] args) {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
}
}
输出结果
我是线程Thread-0
Thread-0结束
我是线程Thread-1
Thread-1结束
public class SynchronizedObjectLock02 implements Callable {
static SynchronizedObjectLock02 instance = new SynchronizedObjectLock02();
Object block1 = new Object();
Object block2 = new Object();
@Override
public Object call() throws InterruptedException {
//这个代码块使用的是第一把锁,当他释放时,后面的代码使用的是第二把锁,因此马上执行
synchronized (block1) {
System.out.println("block1锁,我是线程" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("block1锁,我是线程" + Thread.currentThread().getName() + "结束");
}
synchronized (block2) {
System.out.println("block2锁,我是线程" + Thread.currentThread().getName());
Thread.sleep(3000);
System.out.println("block2锁,我是线程" + Thread.currentThread().getName()+ "结束");
}
return null;
}
public static void main(String[] args) {
FutureTask futureTask01 = new FutureTask(instance);
FutureTask futureTask02 = new FutureTask(instance);
Thread t1 = new Thread(futureTask01);
Thread t2 = new Thread(futureTask02);
t1.start();
t2.start();
}
}
输出结果
block1锁,我是线程Thread-0
block1锁,Thread-0结束
block2锁,我是线程Thread-0 // 可以看到当第一个线程在执行完第一段同步代码块之后,第二个同步代码块可以马上得到执行,因为他们使用的锁不是同一把
block1锁,我是线程Thread-1
block1锁,Thread-1结束
block2锁,Thread-0结束
block2锁,我是线程Thread-1
block2锁,Thread-1结束
- 方法锁形式:synchronized修饰普通方法,锁对象默认为this
public class SynchronizedMethodLock implements Runnable{
static SynchronizedMethodLock instance = new SynchronizedMethodLock();
@Override
public void run() {
method();
}
public synchronized void method() {
System.out.println("我是线程" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "结束");
}
public static void main(String[] args) {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
}
}
- 类锁形式:synchronized修饰静态方法或指定锁对象为Class对象
public class SynchronizedClassLock implements Runnable{
static SynchronizedClassLock instance01 = new SynchronizedClassLock();
static SynchronizedClassLock instance02 = new SynchronizedClassLock();
@Override
public void run() {
// method(); //修饰静态方法
method02(); //锁定对象为Class对象
}
//synchronized修饰静态方法
public synchronized static void method() {
System.out.println("我是线程" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "结束");
}
//synchronized锁定Class对象
public void method02() {
synchronized (SynchronizedClassLock.class) {
System.out.println("我是线程" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "结束");
}
}
public static void main(String[] args) {
Thread t1 = new Thread(instance01);
Thread t2 = new Thread(instance02);
t1.start();
t2.start();
}
}
输出结果
我是线程Thread-0
Thread-0结束
我是线程Thread-1
Thread-1结束
Volatile作用
- 防重排序
- 实现可见性
- 保证原子性:单次读/写
final使用
- 修饰类: 表示不能继承该类,并且final类中所有方法都隐式为final
- 修饰方法: private方法是隐式为final,final方法是可以被重载的
- **修饰参数:**方法参数中修饰基本类型表示方法体中参数值不能更改,修饰引用类型表示引用对象是不能更改
- **修饰变量:**表示初始化之后无法更改
Java IO/NIO/AIO知识体系
IO理解分类 -传输方式
字节流是计算机看的,字符是给人看的。
- 字节流
- 字符流
区别:
- 字节读取单个字节,字符读取单个字符(UTF-8编码中文汉字是3个字节,GBK中文汉字是2个字节)
- 字节流用来处理二进制文件(图片、MP3、视频文件),字符流用来处理文本文件(可以看做是特殊的二进制文件,使用了某种编码,人可以阅读)
**字节转字符 **
编码就是把字符转换为字节,解码就是把字节重新组合成字符。
GBK编码:中文字符占2个字节,英文占1个字节
UTF-8编码中,中文字符占3个字节,英文字符占1个字节
UTF-16be编码中国,中文和英文字符都占2个字节
IO理解分类 -数据操作
- 文件
FileInputStream、FileOutputStream、FileReader、FileWriter - 数组
ByteArrayInputStream、ByteArrayOutputStream、CharArrayReader、CharArrayWriter - 管道操作
PipedInputStream、PipedInputStream、PipedReader、PipedWriter - 基础数据类型
DataInputStream、DataOutputStream - 缓冲操作
BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter - 打印
PrintStream、PrintWriter - 对象序列化反序列化
ObjectInputStream、ObjectOutputStream
-转换
InputStreamReader、OutputStreamWriter
I/O使用了装饰者模式来实现
以 InputStream 为例,
- InputStream 是抽象组件;
- FileInputStream 是 InputStream 的子类,属于具体组件,提供了字节流的输入操作;
- FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能。例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。实例化一个具有缓存功能的字节流对象时,只需要在 FileInputStream 对象上再套一层 BufferedInputStream 对象即可。
FileInputStream fileInputStream = new FileInputStream(filePath);
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
DataInputStream 装饰者提供了对更多数据类型进行输入的操作,比如 int、double 等基本类型。
I/O常见类的使用
- 磁盘操作:File
- 字节操作:InputStream和OutputStream
- 字符操作:Reader和Writer
- 对象操作:Serializable
- 网络操作:Socket
File相关(读取目录下所有文件)
public class IOFileExample {
public static void listAllFiles(File dir) {
if(dir == null || !dir.exists()) { //判断文件为空或者不存在
return;
}
if(dir.isFile()) { //判断是否为文件
System.out.println(dir.getName());
return;
}
for (File file: dir.listFiles()) {
listAllFiles(file);
}
}
public static void main(String[] args) {
IOFileExample instance = new IOFileExample();
instance.listAllFiles(new File("C:\\Users\\91358\\IdeaProjects\\untitled\\src\\com\\company"));
}
}
字节流相关(复制文件、逐行输出文本文件内容)
public class IOFileCopyExample {
//复制源文件到目标文件
public static void copyFile(String src, String dist) throws IOException {
FileInputStream in = new FileInputStream(src);
FileOutputStream out = new FileOutputStream(dist);
byte[] buffer = new byte[24 * 1024];
// read() 最多读取 buffer.length 个字节
// 返回的是实际读取的个数
// 返回 -1 的时候表示读到 eof,即文件尾
while(in.read(buffer, 0, buffer.length) != -1) {
out.write(buffer);
}
in.close();
out.close();
}
//从文件中读取所有文本
public static void readFileContent(String filePath) throws IOException {
FileReader fileReader = new FileReader(filePath);
BufferedReader bufferedReader = new BufferedReader(fileReader);
String line;
while((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
// 装饰者模式使得 BufferedReader 组合了一个 Reader 对象
// 在调用 BufferedReader 的 close() 方法时会去调用 Reader 的 close() 方法
// 因此只要一个 close() 调用即可
bufferedReader.close();
}
public static void main(String[] args) throws IOException {
IOFileCopyExample instance = new IOFileCopyExample();
instance.copyFile("C:\\Users\\91358\\IdeaProjects\\untitled\\src\\com\\company\\sourceFile", "C:\\Users\\91358\\IdeaProjects\\untitled\\src\\com\\company\\targetFile");
instance.readFileContent("C:\\Users\\91358\\IdeaProjects\\untitled\\src\\com\\company\\targetFile");
}
}
输出结果
1234567真的吗?太棒了,居然真的可以复制诶。
6666呀
序列化 & Serializable & transient
序列化就是将一个对象转换成字节序列,方便存储和传输。
- 序列化:ObjectOutputStream.writeObject()
- 反序列化:ObjectInputStream.readOject()
public class IOFileSerializableExample {
private static class A implements Serializable{ //序列化的类需要实现Serializable接口,它是一个标准,没有任何方法需要实现,如果不去实现它的话而进行序列化,会跑出异常
private int x;
private String y;
A(int x, String y) {
this.x = x;
this.y = y;
}
@Override
public String toString() {
return "x = " + x + ", y = " + y;
}
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
A a1 = new A(132, "abc");
String objectFile = "C:\\Users\\91358\\IdeaProjects\\untitled\\src\\com\\company\\targetFile";
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(objectFile));
objectOutputStream.writeObject(a1);
objectOutputStream.close();
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(objectFile));
A a2 = (A) objectInputStream.readObject();
objectInputStream.close();
System.out.println(a2);
}
}
输出结果
x = 132, y = abc
transient关键字可以是一些属性不会被序列化
ArrayList 中存储数据的数组 elementData 是用 transient 修饰的,因为这个数组是动态扩展的,并不是所有的空间都被使用,因此就不需要所有的内容都被序列化。通过重写序列化和反序列化方法,使得可以只序列化数组中有内容的那部分数据。
private transient Object[] elementData;
Java 中的网络支持
- InetAddress:用于表示网络上的硬件资源,即IP地址;
- URL:统一资源定位符
- Sockets:使用TCP协议实现网络通信
- Datagram:使用UDP协议实现网络通信
InetAddress
没有公有的构造函数,只能通过静态方法来创建实例。
InetAddress.getByName(String host);
InetAddress.getByAddress(byte[] address);
URL
可以直接从URL中读取字节流数据。
public class IOFileNetworkSupportExample {
public static void main(String[] args) throws IOException {
URL url = new URL("http://www.baidu");
InputStream is = url.openStream();
InputStreamReader isr = new InputStreamReader(is, "UTF-8");
BufferedReader br = new BufferedReader(isr);
String line;
while((line = br.readLine()) != null) {
System.out.println(line);
}
}
}
输出结果
<!DOCTYPE html>
<!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css href=http://s1.bdstatic/r/www/cache/bdorz/baidu.min.css><title>百度一下,你就知道</title></head> <body link=#0000cc> <div id=wrapper> <div id=head> <div class=head_wrapper> <div class=s_form> <div class=s_form_wrapper> <div id=lg> <img hidefocus=true src=//www.baidu/img/bd_logo1.png width=270 height=129> </div> <form id=form name=f action=//www.baidu/s class=fm> <input type=hidden name=bdorz_come value=1> <input type=hidden name=ie value=utf-8> <input type=hidden name=f value=8> <input type=hidden name=rsv_bp value=1> <input type=hidden name=rsv_idx value=1> <input type=hidden name=tn value=baidu><span class="bg s_ipt_wr"><input id=kw name=wd class=s_ipt value maxlength=255 autocomplete=off autofocus></span><span class="bg s_btn_wr"><input type=submit id=su value=百度一下 class="bg s_btn"></span> </form> </div> </div> <div id=u1> <a href=http://news.baidu name=tj_trnews class=mnav>新闻</a> <a href=http://www.hao123 name=tj_trhao123 class=mnav>hao123</a> <a href=http://map.baidu name=tj_trmap class=mnav>地图</a> <a href=http://v.baidu name=tj_trvideo class=mnav>视频</a> <a href=http://tieba.baidu name=tj_trtieba class=mnav>贴吧</a> <noscript> <a href=http://www.baidu/bdorz/login.gif?login&tpl=mn&u=http%3A%2F%2Fwww.baidu%2f%3fbdorz_come%3d1 name=tj_login class=lb>登录</a> </noscript> <script>document.write('<a href="http://www.baidu/bdorz/login.gif?login&tpl=mn&u='+ encodeURIComponent(window.location.href+ (window.location.search === "" ? "?" : "&")+ "bdorz_come=1")+ '" name="tj_login" class="lb">登录</a>');</script> <a href=//www.baidu/more/ name=tj_briicon class=bri style="display: block;">更多产品</a> </div> </div> </div> <div id=ftCon> <div id=ftConw> <p id=lh> <a href=http://home.baidu>关于百度</a> <a href=http://ir.baidu>About Baidu</a> </p> <p id=cp>©2017 Baidu <a href=http://www.baidu/duty/>使用百度前必读</a> <a href=http://jianyi.baidu/ class=cp-feedback>意见反馈</a> 京ICP证030173号 <img src=//www.baidu/img/gs.gif> </p> </div> </div> </div> </body> </html>
Java8 特性
函数编程(lambda表达式)
- Lambda表达式在Java中又称为闭包或匿名函数。
- Lambda表达式可以使用静态、非静态、局部变量,又称为Lambda变量捕获。
- Lambda表达式在不修改Lambda表达式提供的参数的情况下,可以用方法引用
list.forEach(n -> System.out.println(n));
list.forEach(System.out::println); // 使用方法引用
//若对参数有任何修改,则不能使用方法引用,而需键入完整地lambda表达式
list.forEach((String s) -> System.out.println("*" + s + "*"));
- Lambda表达式内部不能修改定义表达式外定义的变量。但是可以访问。
- Lambda表达式在编译器内部被翻译成私有方法,并派发invokedynamic字节码指令进行调用。
stream流
stream流分为顺序流和并行流
性能:如果是多核机器,并行流比顺序流快上一倍
顺序流
List <Person> people = list.getStream.collect(Collectors.toList());
并行流
List <Person> people = list.getStream.parallel().collect(Collectors.toList());
内置四大函数接口
- 消费性接口(先消费后供给的原则)
Consumer void accept(T t)有参数,无返回值的抽象方法
Consumer<PersonOne> greeter = (p) -> System.out.println("firstName = " + p.getFirstName());
greeter.accept(new PersonOne("zhang", "san"));
greeter.accept(new PersonOne("li", "si"));
greeter.accept(new PersonOne("wang", "si"));
输出结果
firstName = zhang
firstName = li
firstName = wang
- 供给型接口
Supplier T get()无参有返回值的抽象方法
Supplier<PersonOne> personOneSupplier = PersonOne::new;
System.out.println("Object = " + personOneSupplier.get()); //new PersonOne
输出结果
Object = company.LambdaExample$PersonOne@52cc8049
- 断定型接口
Predicate boolean test(T t)有参,但是返回值类型固定为boolean
Predicate<String> predicate = (s) -> s.length() > 0;
System.out.println("test() = " + predicate.test("foo"));
System.out.println("negate().test() = " + predicate.negate().test("foo"));
Predicate<Boolean> nonNull = Objects::nonNull;
Predicate<Boolean> isNull = Objects::isNull;
Predicate<String> isEmpty = String::isEmpty;
Predicate<String> isNotEmpty = isEmpty.negate();
System.out.println("nonNull = " + nonNull.test(null));
System.out.println("isNull= " + isNull.test(null));
System.out.println("isEmpty = " + isEmpty.test("123"));
System.out.println("isNotEmpty= " + isNotEmpty.test(""));
输出结果:
test() = true
negate().test() = false
nonNull = false
isNull= true
isEmpty = false
isNotEmpty= false
- 函数型接口
Function<T, R> R apply(T t) 有参数有返回值的抽象方法
Function<String, Integer> toInteger = Integer::valueOf;
Function<String, String> backToString = toInteger.andThen(String::valueOf);
System.out.println("valueOf = " + backToString.apply("456"));
System.out.println("valueOf = " + toInteger.apply("123"));
输出结果
valueOf = 456
valueOf = 123
Optional
Optional<String> name = Optional.of("sanaulla"); //为空值初始化Optional
Optional<String> empty = Optional.ofNullable(null); //为非空值初始化Optional,为空初始化Optional为Optional.empty
// Optional<String> someNull = Optional.of(null);
System.out.println(name);
System.out.println(empty);
if(name.isPresent()){ //判断name是否有值
System.out.println(name.get()); //
}
try{
System.out.println(empty.get());
}catch (NoSuchElementException e){
System.out.println(e.getMessage());
}
name.ifPresent((value) ->{ //如果name有值就调用consumer(消费性接口),否则,不处理
System.out.println("The length of the value is: " + value);
});
empty.ifPresent((value) ->{
System.out.println("The length of the value is: " + value);
});
//判断empty是否有值,有值将值返回,没有则返回内容。
System.out.println(empty.orElse("there is no vlaue present"));
System.out.println(name.orElse("there is some value"));
//orElseGet和orElse类似,区别在于得到值,orElse是以传入的字符串作为默认值,orElseGet是以Supplier(供给型接口)作为默认值
System.out.println(empty.orElseGet(() -> "Default value,真的吗?"));
System.out.println(empty);
System.out.println(name.orElseGet(() -> "Default value"));
try{
empty.orElseThrow(ValueAbsentException::new);
}catch (Throwable e) {
System.out.println(e.getMessage());
}
//map方法通过lambda表达式参数对Option值进行修改
Optional<String> upperName = name.map((value) -> value.toUpperCase());
System.out.println(upperName.orElse("No value found"));
System.out.println(name.orElse("No value found"));
//flatMap和map类似,map内的Lambda表达式返回值类型任意,但是flatMap内的表达式返回值类型必须为Optional
upperName = name.flatMap((value) -> Optional.of(value.toUpperCase()));
System.out.println(upperName.orElse("No value found"));
//过滤字符长度大于0的Optional
Optional<String> longName = name.filter((value) -> value.length() > 0);
System.out.println(longName);
输出结果
Optional[sanaulla]
Optional.empty
sanaulla
No value present
The length of the value is: sanaulla
there is no vlaue present
sanaulla
Default value,真的吗?
Optional.empty
sanaulla
No value present in the Optional instance
SANAULLA
sanaulla
SANAULLA
Optional[sanaulla]
default方法
类型注解
重复注解
JVM
类加载
类字节码
源代码通过编译器编译成字节码,在通过类加载子系统加载到JVM中运行
类加载机制
生命周期:加载、验证、准备、解析、初始化
类加载方式:
1、命令行启动应用由JVM初始化加载
2、通过Class.forName()方式动态加载
3、通过ClassLoader.loadClass()方式动态加载
内存结构
10. 线程私有:程序计数器,虚拟机栈、本地方法栈
11. 线程共享:方法区、堆区
在Hotspot JVM中,直接将本地方法栈和虚拟机栈合二为一。
一、程序计数器
作用:用来存储指向下一条字节码指令的地址。
1、每个线程都有一个程序计数器,是线程私有的。生命周期和线程的生命周期一致,是程序控制流的指示器。
2、字节码解释器通过改变计数器选取字节码指令
3、是JVM规范中没有规定任何OutOfMemoryError情况的区域
二、虚拟机栈
对应着一次次Java方法调用,是线程私有的。生命周期和和线程是一致的。Java虚拟机规范Java虚拟机栈的大小是动态的或者是固定不变的。
栈中的数据都是以栈帧的格式存在,在这个线程中每个方法都各自对应的一个栈帧
栈帧的内部结构
由局部变量、操作数栈、动态连接、方法返回地址、附加信息组成。操作数栈主要是用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。动态链接指运行时常量池的方法引用。方法返回地址指方法正常退出或异常退出的地址。
本地方法栈
用于管理Java方法的调用,允许线程固定或者动态扩展内存大小。
堆内存
主要是用于存放对象实例。
逻辑上划分:新生代、老年代、元空间
新生代:新对象和没到达一定年龄的对象都在新生代
老年代:被长时间使用的对象,老年代的内存内存空间比年轻代更大
元空间:像一些操作方法中的临时对象等等。JDK之前是占JVM内存,JDK1.8之后使用物理内存。
对象在堆中的生命周期
堆被分为新生代和老年代,新生代又被进一步分为Eden区和Survivor区,Survivor区由From Survivor区和To Survivor区组成。
创建一个对象优先分配到新生代的Eden区,并且JVM会定义一个对象年轻计数器(-XX:MaxTenuringThreshold)。Eden空间不足时,JVM执行新生代的垃圾回收(Minor GC),把存活的对象转移到Survivor区,年轻计数器+1,Survivor中也会经历Minor GC,每次经历一次MInor GC,年轻计数器都会+1,如果对象超过了最大值(-XX:PetenureSizeThreshold),默认是15次回收标记,对象会直接分配到老年代
针对HotSpot VM的实现,按照回收区域分为:部分收集(Partial GC)、整堆收集(Full GC)
部分收集分为:
- 新生代收集(Minor GC/Young GC):新生代的收集器
- 老年代收集(Major GC/Old GC):老年代的收集器
- 混合收集(Mixed GC):整个新生代收集和部分老年代的垃圾收集
整堆收集:整个Java堆和方法区的垃圾收集
方法区用于存放已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等。
内存模型
垃圾回收
判断一个对象是否会被回收
1、引用计数算法
给对象添加一个引用计数器,当对象增加一个引用计数器+1,引用失效-1,引用计数为0对象被回收。
两个对象出现循环引用的时候,应用计数器永不为0,导致它们无法回收。
正因为循环引用的存在,因此虚拟机不适用引用计数算法。
2、可达性分析算法
通过GC Root作为起始点进行搜索,能够达到的对象都是存活的,不可达对象都是可被回收。
3、方法区的回收
主要是对常量池的回收和对类的卸载。
4、finalize()
通过该方法让对象重新被引用。
引用类型
1、强应用
被强引用关联的对象不会被回收。
使用new一个对象的方式来创建强引用。
Object obj = new Object();
2、软引用
被软引用关联的对象只有内存不够的情况才会被回收。
使用SoftReference类创建软引用
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<>(obj); //obj对象被软引用关联
obj = null;
3、弱引用
被弱引用关联的对象一定会被回收。
使用WeakReference类创建弱引用。
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<>(obj); //obj对象被弱引用关联
obj = null;
4、虚引用
被虚引用关联的对象被回收时会收到一个系统通知。
使用PhantomReference类创建虚引用
Object object = new Object();
ReferenceQueue queue = new ReferenceQueue();
PhantomReference pr = new PhantomReference(object, queue); // 虚引用,必须与一个引用队列关联
垃圾回收
1、标记-清除
将存活的对象进行标记,然后清除掉未被标记的对象。
不足:
- 标记-清除过程中效率都不高
- 会产生大量不连续的内存碎片,导致无法给大对象分配内存
2、标记-整理
让所有存活的对象进行标记,然后直接清理掉边界以外的内存。
3、复制
将内存划分两个大小相等的两块,每次只使用其中一块,当使用的这块内存用完了就会把存活的对象进行复制到另一块。再把使用过的那块内存空间全部清理。
不足:只使用了内存的一半。
4、分代收集
根据存活周期划分不同划分区,不同划分区采用适当收集算法
1、新生代使用:复制算法
2、老年代使用:标记 - 清除 或者 标记 - 整理算法。
垃圾收集器
算法
排序
冒泡排序
从前往后两两比较进行交换后,保证后者比前者大,一次循环完,最大元素就在数列的末尾。重复相同做法遍历,第二大元素被排列到最大元素之前,直到数列有序为止。
public class SortExample {
public static void main(String[] args) {
int[] a = {20,40,30,10,60,50};
System.out.printf("before sort:");
for (int i = 0; i < a.length; i++) {
System.out.printf("%d ", a[i]);
}
System.out.printf("\n");
bubbleSort(a, a.length);
System.out.printf("after sort:");
for (int i = 0; i < a.length; i++) {
System.out.printf("%d ", a[i]);
}
}
//冒泡排序
//从前往后两两比较进行交换后,保证后者比前者大,一次循环完,最大元素就在数列的末尾。
//重复相同做法遍历,第二大元素被排列到最大元素之前,直到数列有序为止。
static void bubbleSort(int[] a, int n){
for(int i=n-1; i > 0; i--) {
for(int j=0; j < i; j++ ) {
if(a[j] > a[j+1]) {
int temp = a[j];
a[j] = a[j+1];
a[j+1] = temp;
}
}
}
}
}
快速排序
从数列中挑出一个基准值。将所有比基准值小摆放到基准前面,所有比基准值大的放在后面,递归的把“基准值前面的子数列”和后面的“基准后面的子数列”进行排序
public class SortExample {
public static void main(String[] args) {
int[] a = {20,40,30,10,60,50};
System.out.printf("before sort:");
for (int i = 0; i < a.length; i++) {
System.out.printf("%d ", a[i]);
}
System.out.printf("\n");
// bubbleSort(a, a.length);
quickSort(a, 0, a.length - 1);
System.out.printf("after sort:");
for (int i = 0; i < a.length; i++) {
System.out.printf("%d ", a[i]);
}
}
//冒泡排序
//从前往后两两比较进行交换后,保证后者比前者大,一次循环完,最大元素就在数列的末尾。
//重复相同做法遍历,第二大元素被排列到最大元素之前,直到数列有序为止。
static void bubbleSort(int[] a, int n){
for(int i=n-1; i > 0; i--) {
for(int j=0; j < i; j++ ) {
if(a[j] > a[j+1]) {
int temp = a[j];
a[j] = a[j+1];
a[j+1] = temp;
}
}
}
}
//快速排序
//挑出一个基准数,保证比基准数小的放在基准数左边,比基准数大的放在基准数右边,分别对基准数两侧的子数列的数据进行递归调用,直到数列有序
static void quickSort(int[] a, int l, int r) {
if(l < r) {
int i = l;
int j = r;
int x = a[i];
//循环保证基准数>左子数列,基准数<右数列
while(i < j) {
while(i < j && a[j] > x) {
j --;
}
if(i < j) {
a[i++] = a[j];
}
while(i < j && a[i] < x){
i ++;
}
if(i < j) {
a[j--] = a[i];
}
}
a[i] = x;
//两侧子数列递归调用
quickSort(a, l, i-1);
quickSort(a, i + 1, r);
}
}
}
更多推荐
Java基础复习
发布评论