ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
# Java简答1 ## 语言类(概念) ### 面向对象思想 todo: * 面向对象、面向过程 * 面向对象的三大基本特征和五大基本原则 #### 面向过程思想概述 我们来回想一下,这几天我们完成一个需求的步骤:首先是搞清楚我们要做什么,然后在分析怎么做,最后我们再代码体现。一步一步去实现,而具体的每一步都需要我们去实现和操作。这些步骤相互调用和协作,完成我们的需求。在上面的每一个具体步骤中我们都是参与者,并且需要面对具体的每一个步骤和过程,这就是面向过程最直接的体现。那么什么是面向过程开发呢? 面向过程开发,其实就是面向着具体的每一个步骤和过程,把每一个步骤和过程完成,然后由这些功能方法相互调用,完成需求。 面向过程的代表语言:C语言 #### 面向对象思想概述 当需求单一,或者简单时,我们一步一步去操作没问题,并且效率也挺高。可随着需求的更改,功能的增多,发现需要面对每一个步骤很麻烦了。这时就开始思索,能不能把这些步骤和功能在进行封装,封装时根据不同的功能,进行不同的封装,功能类似的封装在一起。这样结构就清晰了很多。用的时候,找到对应的类就可以了。这就是面向对象的思想。 #### 面向对象思想特点 a:是一种更符合我们思想习惯的思想 b:可以将复杂的事情简单化 c:将我们从执行者变成了指挥者,角色发生了转换 ### Java基本特征 #### Java是如何实现跨平台 这就要谈及Java虚拟机(Java Virtual Machine,简称 JVM)。 JVM也是一个软件,不同的平台有不同的版本。我们编写的Java源码,编译后会生成一种 .class 文件,称为字节码文件。Java虚拟机就是负责将字节码文件翻译成特定平台下的机器码然后运行。也就是说,只要在不同平台上安装对应的JVM,就可以运行字节码文件,运行我们编写的Java程序。 而这个过程中,我们编写的Java程序没有做任何改变,仅仅是通过JVM这一”中间层“,就能在不同平台上运行,真正实现了”一次编译,到处运行“的目的。 JVM是一个”桥梁“,是一个”中间件“,是实现跨平台的关键,Java代码首先被编译成字节码文件,再由JVM将字节码文件翻译成机器语言,从而达到运行Java程序的目的。 注意:编译的结果不是生成机器码,而是生成字节码,字节码不能直接运行,必须通过JVM翻译成机器码才能运行。不同平台下编译生成的字节码是一样的,但是由JVM翻译成的机器码却不一样。 所以,运行Java程序必须有JVM的支持,因为编译的结果不是机器码,必须要经过JVM的再次翻译才能执行。即使你将Java程序打包成可执行文件(例如 .exe),仍然需要JVM的支持。 注意:跨平台的是Java程序,不是JVM。JVM是用C/C++开发的,是编译后的机器码,不能跨平台,不同平台下需要安装不同版本的JVM。面向对象思想 #### 为什么说 Java 中只有值传递 todo: * 值传递、引用传递 * 为什么说 Java 中只有值传递 ### Java的四大特性 封装、继承、多态、(抽象)。也有说三大特征的,没了抽象 #### 封装 通常我们将某事物的属性和行为包装到对象中,隐藏对象的属性和实现细节,仅对外提供公共访问方式。 这样做有这些好处: - 隐藏实现细节,提供公共的访问方式 - 提高代码复用性 - 提高安全性[禁止对象之间的不良交互提高模块化] #### 继承 多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只要继承那个类即可。通过使用继承我们能够非常方便地复用以前的代码。 **继承的好处** - a:提高了代码的复用性 - b:提高了代码的维护性 - c:让类与类之间产生了关系,是多态的前提 **继承的弊端** 使用继承是,有一个很明显高的弊端就是类的耦合性增强了。 **关于继承如下 3 点请记住:** 1. 子类拥有父类非 private 的成员方法和成员变量)。 2. 子类不能继承父类的构造方法,但是可以通过super关键字去访问父类构造方法 3. 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。 4. 子类可以用自己的方式实现父类的方法(重写)。 #### 多态 在Java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。 多态是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性 #### 抽象 现实世界的事物进行概括,抽象为在计算机虚拟世界中有意义的实体 ### 多态 多态性就是相同的消息使得不同的类做出不同的响应。 #### 多态实现条件 Java实现多态有三个必要条件:继承、重写、向上转型: - **继承**。在多态中必须存在有继承关系的子类和父类。 - **重写**。子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。 - **向上转型**。在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。 #### 多态的实现方式有哪些? todo:这几句解释,我自己都看不太明白。。 **1、基于继承实现的多态** 基于继承的实现机制,主要表现在父类和继承该父类的一个或多个子类对某些方法的重写,多个子类对同一方法的重写可以表现出不同的行为。多态的表现就是不同的对象可以执行相同的行为,但是他们都需要通过自己的实现方式来执行,这就要得益于向上转型了。 **2、基于接口实现的多态** 继承是通过重写父类的同一方法的几个不同子类来体现的,那么就可就是通过实现接口并覆盖接口中同一方法的几不同的类体现的。 在接口的多态中,指向接口的引用必须是指定这实现了该接口的一个类的实例程序,在运行时,根据对象引用的实际类型来执行对应的方法。 继承都是单继承,只能为一组相关的类提供一致的服务接口。但是接口可以是多继承多实现,它能够利用一组相关或者不相关的接口进行组合与扩充,能够对外提供一致的服务接口。所以它相对于继承来说有更好的灵活性。 #### 多态有哪些弊端 todo: ### 接口和抽象类 #### 接口、抽象的区别 - 多继承。一个类可以实现多个接口,但只能单继承一个抽象类 - 方法。接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),抽象类可以有非抽象的方法。 - 变量。接口的变量默认(必须)是 static final类型;抽象类则没有限制 - 实例化。接口不能用 new 实例化,但可以声明,但是必须引用一个实现该接口的对象 - 设计层面。设计层面来说,抽象是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。 #### 接口意义 接口更多的意义是 一种行为规范: * 2、简单、规范性:如果一个项目比较庞大,那么就需要一个能理清所有业务的架构师来定义一些主要的接口,这些接口不仅告诉开发人员你需要实现那些业务,而且也将命名规范限制住了(防止一些开发人员随便命名导致别的程序员无法看明白)。 * 3、维护、拓展性:比如你要做一个画板程序,其中里面有一个面板类,主要负责绘画功能,然后你就这样定义了这个类。可是在不久将来,你突然发现这个类满足不了你了,然后你又要重新设计这个类,更糟糕是你可能要放弃这个类,那么其他地方可能有引用他,这样修改起来很麻烦。如果你一开始定义一个接口,把绘制功能放在接口里,然后定义类时实现这个接口,然后你只要用这个接口去引用实现它的类就行了,以后要换的话只不过是引用另一个类而已,这样就达到维护、拓展的方便性。 * 4、安全、严密性:接口是实现软件松耦合的重要手段,它描叙了系统对外的所有服务,而不涉及任何具体的实现细节。这样就比较安全、严密一些(一般软件服务商考虑的比较多)。 #### 抽象意义 抽象更多是为了复用 逻辑: - 1.因为抽象类不能实例化对象,所以必须要有子类来实现它之后才能使用。这样就可以把一些具有相同属性和方法的组件进行抽象,这样更有利于代码和程序的维护。 - 2.当又有一个具有相似的组件产生时,只需要实现该抽象类就可以获得该抽象类的那些属性和方法。 #### 如何选择 从设计层面上看,抽象类提供了一种 IS-A 关系,那么就必须满足里式替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 LIKE-A 关系,它只是提供一种方法实现契约,并不要求接口和实现接口的类具有 IS-A 关系 * 使用接口: * 需要让不相关的类都实现一个方法,例如不相关的类都可以实现 Compareable 接口中的 compareTo() 方法; * 需要使用多重继承。 * 使用抽象类: * 需要在几个相关的类中共享代码。 * 需要能控制继承来的成员的访问权限,而不是都为 public。 * 需要继承非静态和非常量字段。 ### 重载和重写 ##### 重载和重写的区别 **重载** - 发生在同一个类中, - 方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同, - 发生在编译时。 **重写** - 发生在父子类中, - 方法名、参数列表必须相同, - 返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类; - 如果父类方法访问修饰符为 private 则子类就不能重写该方法。 ##### 重载和重写绑定机制有何区别 **重载**:类内多态,静态绑定机制(编译时已经知道具体执行哪个方法),方法同名,参数不同 **重写**:类间多态,动态绑定机制(运行时确定),实例方法,两小两同一大(方法签名相同,子类的方法所抛出的异常、返回值的范围不大于父类的对应方法,子类的方法可见性不小于父类的对应方法)方法签名相同,子类的方法所抛出的异常、返回值的范围不大于父类的对应方法,子类的方法可见性不小于父类的对应方法。 ##### 父类的静态方法能否被子类重写 - 父类的静态方法是不能被子类重写的,其实重写只能适用于实例方法,不能用于静态方法,对于上面这种静态方法而言,我们应该称之为隐藏。 - Java静态方法形式上可以重写,但从本质上来说不是Java的重写。因为静态方法只与类相关,不与具体实现相关。声明的是什么类,则引用相应类的静态方法(本来静态无需声明,可以直接引用)。并且static方法不是后期绑定的,它在编译期就绑定了。换句话说,这个方法不会进行多态的判断,只与声明的类有关。 ### 常见修饰符 **Java中常见的修饰符** - 权限修饰符:private,默认的,protected,public - 状态修饰符:static,final - 抽象修饰符:abstract **适用于类** - 权限修饰符:默认修饰符,public - 状态修饰符:final、(内部类可用static) - 抽象修饰符:abstract **适用于构造方法** - 权限修饰符:private,默认的,protected,public **适用于成员变量** - 权限修饰符:private,默认的,protected,public - 状态修饰符:static,final **适用于成员方法** - 权限修饰符:private,默认的,protected,public - 状态修饰符:static,final - 抽象修饰符:abstract **范围权限修饰符** java中的访问权限修饰符:public、private、protected,已经默认的default 类的成员不写访问修饰时默认为default。默认对于同一个包中的其他类相当于公开(public),对于不是同一个包中的其他类相当于私有(private)。受保护(protected)对子类相当于公开,对不是同一包中的没有父子关系的类相当于私有。 | 作用域 | 当前类 | 同包 | 子类 | 其他 | | --------- | :----: | :--: | :--: | :--: | | public | o | o | o | o | | protected | o | o | o | x | | default | o | o | x | x | | private | o | x | x | x | ### 内部类 关于这部分的详细解答可以参考这边文章[从反编译深入理解JAVA内部类类结构以及final关键字](https://blog.csdn.net/Davidluo001/article/details/50377919) #### 定义 内部类就是定义在另外一个类里面的类。它隐藏在外部类中,封装性更强,不允许除外部类外的其他类访问它;但它可直接访问外部类的成员(持有外类的this指针)。 #### 分类 **成员内部类** 成员内部类是外围类的一个成员,是依附于外围类的. 编译后class文件的命名格式:主类+$+内部类名. 通过反编译代码,我门会发现,即便我们在定义的内部类的构造器是无参构造器,编译器还是会默认添加一个参数,该参数的类型为指向外部类对象的一个引用,所以成员内部类中的Outter this&0 指针便指向了外部类对象,因此可以在成员内部类中随意访问外部类的成员。从这里也间接说明了成员内部类是依赖于外部类的, 这也引出另一个问题,如果在外部类没有人引用的时候,而成员内部类有人引用,外部类因为被内部类引用所以不会被回收。这就会造成Android中常见的Activity内存泄露。 **静态内部类** 静态内部类,就是修饰为static的内部类,该内部类对象不依赖于外部类对象,就是说我们可以直接创建内部类对象,但其只可以直接访问外部类的所有静态成员和静态方法; 编译后class文件的命名格式:主类+.+内部类名 **匿名内部类** 匿名内部类: 就是局部内部类的简化写法。本质上来说,它是一个继承了该类或者实现了该接口的子类匿名对象. 编译后class文件的命名格式:主类+$+(1,2,3....) 常用格式如下: ``` new 类名或者接口名(){ 重写方法; } ; ``` 定义匿名内部类的前提是,内部类必须要继承一个类或者实现接口,格式为 new 父类或者接口(){定义子类的内容(如函数等)}。也就是说,匿名内部类最终提供给我们的是一个 匿名子类的对象。 **局部内部类** 局部内部类是嵌套在方法和作用域内的,对于这个类的使用主要是应用与解决比较复杂的问题,想创建一个类来辅助我们的解决方案,到那时又不希望这个类是公共可用的,所以就产生了局部内部类,局部内部类和成员内部类一样被编译,只是它的作用域发生了改变,它只能在该方法和属性中被使用,出了该方法和属性就会失效。 #### 静态内部类和非静态内部类的区别 静态内部类与非静态内部类之间存在一个最大的区别:**非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围内,但是静态内部类却没有** 没有这个引用就意味着: - 它的创建是不需要依赖于外围类的。 - 它不能使用任何外围类的非static成员变量和方法。 官方一点的语言就是: - 实例化。静态内部类是指被声明为static的内部类,可不依赖外部类实例化;而非静态内部类需要通过生成外部类来间接生成。 - 访问。静态内部类只能访问外部类的静态成员变量和静态方法,而非静态内部类由于持有对外部类的引用,可以访问外部类的所用成员 #### 为什么内部类调用的外部变量必须是final修饰的? **简单解答** **生命周期**。由于方法中的局部变量的生命周期很短,一旦方法结束变量就要被销毁,为了保证在内部类中能找到外部局部变量,通过final关键字可得到一个外部变量的引用; **一致性**。通过final关键字也不会在内部类去做修改该变量的值,保护了数据的一致性。 **详细一点** **生命周期的原因**。方法中的局部变量(保存在栈帧中),方法结束后这个变量就要释放掉,final保证这个变量始终指向一个对象。首先,内部类和外部类其实是处于同一个级别,内部类不会因为定义在方法中就会随着方法的执行完毕而跟随者被销毁。问题就来了,如果外部类的方法中的变量不定义final,那么当外部类方法执行完毕的时候,这个局部变量肯定也就被GC了,然而内部类的某个方法还没有执行完,这个时候他所引用的外部变量已经找不到了。 **数据一致性**。如果定义为final,java会将这个变量复制一份作为成员变量内置于内部类中,这样的话,由于final所修饰的值始终无法改变,所以这个变量所指向的内存区域就不会变。为了解决:局部变量的生命周期与局部内部类的对象的生命周期的不一致性问题 这里先上一段代码进行说明: ``` public void test(final int b) { final int a = 10; new Thread(){ public void run() { System.out.println(a); }; }.start(); } ``` **生命周期不一致** 方法中的局部变量(保存在栈帧中),当方法执行完毕之后,局部变量a的生命周期就结束了,而此时Thread对象的生命周期很可能还没有结束,Thread的run方法中继续访问变量a就变成不可能了。 为了实现这样的效果,Java采用了 **复制** 的手段来解决这个问题: * 如果局部变量的值在编译期间就可以确定,则直接在匿名内部里面创建一个拷贝。 * 如果局部变量的值无法在编译期间确定,则通过构造器传参的方式来对拷贝进行初始化赋值。 但是新的问题又来了, 既然在run方法中访问的变量a和test方法中的变量a不是同一个变量,当在run方法中改变变量a的值的话,会出现什么情况? 对,会造成数据不一致性,这样就达不到原本的意图和要求。为了解决这个问题,java编译器就限定必须将变量a限制为final变量,不允许对变量a进行更改(对于引用类型的变量,是不允许指向新的对象),这样数据不一致性的问题就得以解决了。 ### 变量 #### 静态变量和实例变量的区别 **静态变量**是被static修饰符修饰的变量,也称为类变量,它属于类,不属于类的任何一个对象,一个类不管创建多少个对象,静态变量在内存中有且仅有一个拷贝。静态变量可以实现让多个对象共享内存。在Java开发中,上下文类和工具类中通常会有大量的静态成员。 **实例变量**必须依存于某一实例,需要先创建对象然后通过对象才能访问到它 #### 成员变量与局部变量 **1.从语法形式上看** 成员变量是属于类的,而局部变量是在方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰; 但是,成员变量和局部变量都能被 final 所修饰; **2.从变量在内存中的存储方式来看** 成员变量是对象的一部分,而对象存在于堆内存,局部变量存在于栈内存 3.从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。 4.成员变量如果没有被赋初值,则会自动以类型的默认值而赋值(一种情况例外被 final 修饰但没有被 static 修饰的成员变量必须显示地赋值);而局部变量则不会自动赋值。 ### 代码块 #### 定义 在Java中,使用{}括起来的代码被称为代码块。 根据其位置和声明的不同,可以分为: * **局部代码块**。在方法中出现;限定变量生命周期,及早释放,提高内存利用率 * **构造代码块**。类中直接用{}定义,每一次创建对象时执行;一般都是为了将多个构造方法方法中相同的代码存放到一起,每次调用构造都执行,并且在构造方法前执行 * **静态代码块**。在类中方法外出现,加了static修饰。jvm加载类时执行,仅执行一次。 * **同步代码块**。 #### 类内代码块执行顺序 对于一个类而言,按照如下顺序执行: * 执行静态代码块 * 执行构造代码块 * 执行构造函数 对于静态变量、静态初始化块、变量、初始化块、构造器,它们的初始化顺序依次是 * 静态变量、静态初始化块 * 变量、初始化块 * 构造器 上面三个等级中,同级的执行顺序,根据在类的位置而定,至上而下执行。 #### 继承类代码块的执行顺序 ![](http://ww1.sinaimg.cn/large/005Ogmrtly1g1obesqwh1j30gg0d1gme.jpg) 当涉及到继承时,按照如下顺序执行: 1. 执行父类的静态代码块、初始化父类静态成员变量。 2. 执行子类的静态代码块、初始化子类静态成员变量。 3. 执行父类的构造代码块、初始化父类普通成员变量;执行父类的构造函数。 4. 执行子类的构造代码块、 初始化子类普通成员变量;执行子类的构造函数。 #### 执行顺序释疑 这里通过对类加载过程的简述来说明 上面提到的执行顺序: 1. 访问SubClass.main(),(这是一个static方法),于是装载器就会为你寻找已经编译的SubClass类的代码(也就是SubClass.class文件)。在装载的过程中,装载器注意到它有一个基类(也就是extends所要表示的意思),于是它再装载基类。不管你创不创建基类对象,这个过程总会发生。如果基类还有基类,那么第二个基类也会被装载,依此类推。 2. 执行根基类的static初始化,然后是下一个派生类的static初始化,依此类推。这个顺序非常重要,因为派生类的“static初始化”有可能要依赖基类成员的正确初始化。 3. 当所有必要的类都已经装载结束,开始执行main()方法体,并用new SubClass()创建对象。 4. 类SubClass存在父类,则调用父类的构造函数,你可以使用super来指定调用哪个构造函数。基类的构造过程以及构造顺序,同派生类的相同。首先基类中各个变量按照字面顺序进行初始化,然后执行基类的构造函数的其余部分。 5. 对子类成员数据按照它们声明的顺序初始化,执行子类构造函数的其余部分。 ### 动态代理是基于什么原理? 36讲-6 ## 语言类(具体) ### Java基本数据类型 | 类型 | 位宽 | 范围 | | :------ | :------------ | :----------------- | | byte | 1个字节,8位 | -128到127之间 | | short | 2个字节,16位 | 2^15 到 2^15 -1 | | int | 4个字节,32位 | -2^31 到 2^31 -1 | | long | 8个字节,64位 | -2^63 到 2^63 -1 | | float | 4个字节,32位 | | | double | 8个字节,64位 | | | char | 2个字节,16位 | 1个汉字刚好2个字节 | | boolean | 1个字节 | true和false | ### 运算符 a+ +和- -a区别 ### Object公有方法 - `equals()`: 和==作用相似 - `hashCode()`:用于哈希查找,重写了equals()一般都要重写该方法 - `getClass()`: 获取Class对象 - `wait()`:让当前线程进入等待状态,并释放它所持有的锁 - `notify()`、`notifyAll()`: 唤醒一个(所有)正处于等待状态的线程 - `toString()`:转换成字符串 - `clone()`:进行对象拷贝 ### 包装类 #### int和Integer有什么区别 * Integer是int的包装类,int则是java的一种基本数据类型 * Integer变量必须实例化后才能使用,而int变量不需要 * Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int则是直接存储数据值 * Integer的默认值是null,int的默认值是0 #### 自动装拆箱 **装箱**:自动将 基本数据类型 转换为 包装器类型; **拆箱**:自动将 包装器类型 转换为 基本数据类型; ```java //拆箱 int yc = 5; //装箱 Integer yc = 5; ``` #### JDK中如何操作装箱、拆箱 在JDK中,装箱过程是通过调用包装器的`valueOf`方法实现的,而拆箱过程是通过调用包装器的`xxxValue`方法实现的(xxx代表对应的基本数据类型)。 Integer、Short、Byte、Character、Long 这几个类的valueOf方法的实现是类似的,有限可列举,共享常量池[-128,127]; Double、Float的valueOf方法的实现是类似的,无限不可列举,不共享; Boolean的valueOf方法的实现不同于以上的整型和浮点型,只有两个值,有限可列举,共享; #### 什么时候装箱/拆箱 什么时候拆箱主要取决于:在当前场景下,你需要的是引用类型还是基本类型。若需要引用类型,但传进来的值是基本类型,则自动装箱(例如,使用equals方法时传进来原生类型的值);若需要的是原生类型,但传进来的值是引用类型,则自动拆箱(例如,使用运算符进行运算时,操作数是包装类型) #### 为何要引用基本数据包装类? **原始数据类型和引用类型局限性**: * 原始数据类型和 Java 泛型并不能配合使用 * Java 的泛型某种程度上可以算作伪泛型,它完全是一种编译期的技巧,Java 编译期会自动将类型转换为对应的特定类型,这就决定了使用泛型,必须保证相应类型可以转换为Object。 **为何要引用基本数据包装类** 就比如,我们使用泛型,需要用到基本数据类型的包装类。 Java 的对象都是引用类型,如果是一个原始数据类型数组,它在内存里是一段连续的内存,而对象数组则不然,数据存储的是引用,对象往往是分散地存储在堆的不同位置。这种设计虽然带来了极大灵活性,但是也导致了数据操作的低效,尤其是无法充分利用现代 CPU 缓存机制。 Java 为对象内建了各种多态、线程安全等方面的支持,但这不是所有场合的需求,尤其是数据处理重要性日益提高,更加高密度的值类型是非常现实的需求。 #### 基本类型的缓冲池 基本类型对应的缓冲池如下: boolean values true and false all byte values short values between -128 and 127 int values between -128 and 127 char in the range \u0000 to \u007F 在使用这些基本类型对应的包装类型时,就可以直接使用缓冲池中的对象。 #### 包装类int的常见问题 1、由于Integer变量实际上是对一个Integer对象的引用,所以两个通过new生成的Integer变量永远是不相等的(因为new生成的是两个对象,其内存地址不同)。 ```java Integer i = new Integer(100); Integer j = new Integer(100); System.out.print(i == j); //false ``` 2、Integer变量和int变量比较时,只要两个变量的值是向等的,则结果为true(因为包装类Integer和基本数据类型int比较时,java会自动拆包装为int,然后进行比较,实际上就变为两个int变量的比较) ```java Integer i = new Integer(100); int j = 100; System.out.print(i == j); //true ``` 3、非new生成的Integer变量和new Integer()生成的变量比较时,结果为false。(因为非new生成的Integer变量指向的是java常量池中的对象,而new Integer()生成的变量指向堆中新建的对象,两者在内存中的地址不同) ```java Integer i = new Integer(100); Integer j = 100; System.out.print(i == j); //false ``` 4、对于两个非new生成的Integer对象,进行比较时,如果两个变量的值在区间-128到127之间,则比较结果为true,如果两个变量的值不在此区间,则比较结果为false ```java Integer i = 100; Integer j = 100; System.out.print(i == j); //true Integer i = 128; Integer j = 128; System.out.print(i == j); //false ``` 对于第4条的原因: java在编译Integer i = 100 ;会自动装箱,会翻译成为Integer i = Integer.valueOf(100);,而java API中对Integer类型的valueOf的定义如下: ```java public static Integer valueOf(int i){ assert IntegerCache.high >= 127; if (i >= IntegerCache.low && i <= IntegerCache.high){ return IntegerCache.cache[i + (-IntegerCache.low)]; } return new Integer(i); } ``` java对于-128到127之间的数,会进行缓存,Integer i = 127时,会将127进行缓存,下次再写Integer j = 127时,就会直接从缓存中取,就不会new了 在 Java 8 中,Integer 缓存池的大小默认为 -128~127。 ```java static final int low = -128; static final int high; static final Integer cache[]; static { // high value may be configured by property int h = 127; String integerCacheHighPropValue = sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); if (integerCacheHighPropValue != null) { try { int i = parseInt(integerCacheHighPropValue); i = Math.max(i, 127); // Maximum array size is Integer.MAX_VALUE h = Math.min(i, Integer.MAX_VALUE - (-low) -1); } catch( NumberFormatException nfe) { // If the property cannot be parsed into an int, ignore it. } } high = h; cache = new Integer[(high - low) + 1]; int j = low; for(int k = 0; k < cache.length; k++) cache[k] = new Integer(j++); // range [-128, 127] must be interned (JLS7 5.1.7) assert IntegerCache.high >= 127; } ``` #### AtomicInteger底层实现原理是什么? 36讲-22 ### 深克隆、浅克隆 #### 如何实现对象克隆 - 有两种方式: - 1.实现Cloneable接口并重写Object类中的clone()方法; - 2.实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆,代码如下。 - 注意问题: - 基于序列化和反序列化实现的克隆不仅仅是深度克隆,更重要的是通过泛型限定,可以检查出要克隆的对象是否支持序列化,这项检查是编译器完成的,不是在运行时抛出异常,这种是方案明显优于使用Object类的clone方法克隆对象。 - 代码如下所示 ```java public class MyUtil { private MyUtil() { throw new AssertionError(); } public static <T> T clone(T obj) throws Exception { ByteArrayOutputStream bout = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bout); oos.writeObject(obj); ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bin); return (T) ois.readObject(); // 说明:调用ByteArrayInputStream或ByteArrayOutputStream对象的close方法没有任何意义 // 这两个基于内存的流只要垃圾回收器清理对象就能够释放资源 } } class CloneTest { public static void main(String[] args) { try { Person p1 = new Person("Hao LUO", 33, new Car("Benz", 300)); Person p2 = MyUtil.clone(p1); // 深度克隆 p2.getCar().setBrand("BYD"); // 修改克隆的Person对象p2关联的汽车对象的品牌属性 // 原来的Person对象p1关联的汽车不会受到任何影响 // 因为在克隆Person对象时其关联的汽车对象也被克隆了 System.out.println(p1); } catch (Exception e) { e.printStackTrace(); } } } ``` 通过反射获得泛型的实际类型参数 - 把泛型变量当成方法的参数,利用Method类的getGenericParameterTypes方法来获取泛型的实际类型参数 - 例子: ```java public class GenericTest { public static void main(String[] args) throws Exception { getParamType(); } /*利用反射获取方法参数的实际参数类型*/ public static void getParamType() throws NoSuchMethodException{ Method method = GenericTest.class.getMethod("applyMap",Map.class); //获取方法的泛型参数的类型 Type[] types = method.getGenericParameterTypes(); System.out.println(types[0]); //参数化的类型 ParameterizedType pType = (ParameterizedType)types[0]; //原始类型 System.out.println(pType.getRawType()); //实际类型参数 System.out.println(pType.getActualTypeArguments()[0]); System.out.println(pType.getActualTypeArguments()[1]); } /*供测试参数类型的方法*/ public static void applyMap(Map<Integer,String> map){ } } ``` - 输出结果: ```java java.util.Map<java.lang.Integer, java.lang.String> interface java.util.Map class java.lang.Integer class java.lang.String ``` #### ### 泛型 关于泛型更详细的说明,可以查看[专案-浅谈泛型] #### 泛型由来 我们的集合可以存储多种数据类型的元素,那么在存储的时候没有任何问题,但是在获取元素,并向下转型的时候,可能会存在一个错误,而这个错误就是ClassCastException . 很显然,集合的这种可以存储多种数据类型的元素的这个特点,不怎么友好 , 程序存在一些安全隐患,那么为了出来这种安全隐患,我们应该限定一个集合存储元素的数据类型,我们只让他存储统一中数据类型的元素,那么在做向下转型的是就不会存在这种安全隐患了. 怎么限定集合只能给我存储同一种数据类型的元素呢? 需要使用泛型。 泛型的使用把类型明确的工作推迟到创建对象或者调用方法的时候才去明确的特殊的类型。参数化类型,把类型当作参数一样的传递。 泛型的出现减少了很多强转的操作,同时避免了很多运行时的错误,在编译期完成检查类型转化 **好处**: - (1):把运行时期的问题提前到了编译期间 - (2):避免了强制类型转换 - (3):优化了程序设计,解决了黄色警告线 #### 什么是泛型擦除? 泛型是只是提供给javac编译器使用的,用来限定集合的输入类型,编译器编译带类型说明的集合时会擦掉“类型”信息。 简单来说,泛型仅在编译器有效,编译生成的.class文件中,并没有相关的类型信息。 #### 泛型通配符 **为什么要使用通配符?** 通配符的设计存在一定的场景,例如在使用泛型后,首先声明了一个Animal的类,而后声明了一个继承Animal类的Cat类,显然Cat类是Animal类的子类,但是List却不是List的子类型,而在程序中往往需要表达这样的逻辑关系。为了解决这种类似的场景,在泛型的参数类型的基础上新增了通配符的用法。 **<? extends T> 上界通配符** 上界通配符顾名思义,<? extends T>表示的是类型的上界(**包含自身**),因此通配的参数化类型可能是T或T的子类。正因为无法确定具体的类型是什么,add方法受限(可以添加null,因为null表示任何类型),但可以从列表中获取元素后赋值给父类型。 **<? super T> 下界通配符** 下界通配符<? super T> ,表示的是参数化类型是T的超类型(**包含自身**),层层至上,直至Object,编译器无从判断get()返回的对象的类型是什么,因此get()方法受限。但是可以进行add()方法,add()方法可以添加T类型和T类型的子类型 **<?> 无界通配符** 任意类型,如果没有明确,那么就是Object以及任意的Java类了 无界通配符用<?>表示,?代表了任何的一种类型,能代表任何一种类型的只有null(Object本身也算是一种类型,但却不能代表任何一种类型,所以List和List的含义是不同的,前者类型是Object,也就是继承树的最上层,而后者的类型完全是未知的) #### 如何获取泛型的具体的类型 把泛型变量当成方法的参数,利用Method类的getGenericParameterTypes方法来获取泛型的实际类型参数 ```java public class GenericTest { public static void main(String[] args) throws Exception { getParamType(); } /*利用反射获取方法参数的实际参数类型*/ public static void getParamType() throws NoSuchMethodException{ Method method = GenericTest.class.getMethod("applyMap",Map.class); //获取方法的泛型参数的类型 Type[] types = method.getGenericParameterTypes(); System.out.println(types[0]); //参数化的类型 ParameterizedType pType = (ParameterizedType)types[0]; //原始类型 System.out.println(pType.getRawType()); //实际类型参数 System.out.println(pType.getActualTypeArguments()[0]); System.out.println(pType.getActualTypeArguments()[1]); } /*供测试参数类型的方法*/ public static void applyMap(Map<Integer,String> map){ } } //输出结果 // java.util.Map<java.lang.Integer, java.lang.String> // interface java.util.Map // class java.lang.Integer // class java.lang.String ``` ### 线程安全类型 #### 如何验证int类型是否线程安全? int类型并不是线程安全的。int 作为基本类型,直接存储在内存栈,且对其进行+,-操作以及++,–操作都不是原子操作,都有可能被其他线程抢断,所以不是线程安全。int 用于单线程变量存取,开销小,速度快。 ```java int count = 0; private void startThread() { for (int i = 0;i < 200; i++){ new Thread(new Runnable() { @Override public void run() { for (int k = 0; k < 50; k++){ count++; } } }).start(); } // 休眠10秒,以确保线程都已启动 try { Thread.sleep(1000*10); } catch (InterruptedException e) { e.printStackTrace(); }finally { Log.e("打印日志----",count+""); } } //期望输出10000,最后输出的是9818 //注意:打印日志----: 9818 ``` #### 哪些类型是线程安全的? Java自带的线程安全的基本类型包括: AtomicInteger, AtomicLong, AtomicBoolean, AtomicIntegerArray,AtomicLongArray等 如何实现? AtomicInteger类中有有一个变量valueOffset,用来描述AtomicInteger类中value的内存位置 。当需要变量的值改变的时候,先通过get()得到valueOffset位置的值,也即当前value的值.给该值进行增加,并赋给next。compareAndSet()比较之前取到的value的值当前有没有改变,若没有改变的话,就将next的值赋给value,倘若和之前的值相比的话发生变化的话,则重新一次循环,直到存取成功,通过这样的方式能够保证该变量是线程安全的 value使用了volatile关键字,使得多个线程可以共享变量,使用volatile将使得VM优化失去作用,在线程数特别大时,效率会较低。 ```java private static AtomicInteger atomicInteger = new AtomicInteger(1); static Integer count1 = Integer.valueOf(0); private void startThread1() { for (int i = 0;i < 200; i++){ new Thread(new Runnable() { @Override public void run() { for (int k = 0; k < 50; k++){ // getAndIncrement: 先获得值,再自增1,返回值为自增前的值 count1 = atomicInteger.getAndIncrement(); } } }).start(); } // 休眠10秒,以确保线程都已启动 try { Thread.sleep(1000*10); } catch (InterruptedException e) { e.printStackTrace(); }finally { Log.e("打印日志----",count1+""); } } //期望输出10000,最后输出的是10000 //注意:打印日志----: 10000 //AtomicInteger使用了volatile关键字进行修饰,使得该类可以满足线程安全。 private volatile int value; public AtomicInteger(int initialValue) { value = initialValue; } ``` ### hashcode与equal #### hashcode与equal区别 * equals()比较两个对象的地址值是否相等 ;hashCode()得到的是对象的存储位置,可能不同对象会得到相同值 * 有两个对象,若equals()相等,则hashcode()一定相等;hashcode()不等,则equals()一定不相等;hashcode()相等,equals()可能相等、可能不等 * 使用equals()比较两个对象是否相等,效率较低,最快办法是先用hashCode()比较,如果hashCode()不相等,则这两个对象肯定不相等;如果hashCode()相等,此时再用equal()比较,如果equal()也相等,则这两个对象的确相等。 #### 为什么要同时重写hashCode和equals两个方法 #### 请手写equal方法,讲讲具体的原理? 代码如下所示,如果是手写代码,一定要弄清楚逻辑思路! ```java public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String) anObject; int n = count; if (n == anotherString.count) { int i = 0; while (n-- != 0) { if (charAt(i) != anotherString.charAt(i)) return false; i++; } return true; } } return false; } ``` ### final、finally、finalize #### 各自作用 * final可以修饰类,方法,变量 * final修饰类代表类不可以继承拓展 * final修饰变量表示变量不可以修改 * final修饰方法表示方法不可以被重写 * finally则是Java保证重点代码一定要被执行的一种机制 * 可以使用 try-finally 或者 try-catch-finally 来进行类似关闭 JDBC连接、保证 unlock 锁等动作。 * finalize 是基础类 java.lang.Object的一个方法 * 它的设计目的是保证对象在被垃圾收集前完成特定资源的回收。finalize 机制现在已经不推荐使用,并且在 JDK 9开始被标记为 deprecated。 #### final **final修饰类、方法** 将方法或者类声明为 final,这样就可以明确告知别人,这些行为是不许修改的。 如果你关注过 Java 核心类库的定义或源码, 有没有发现java.lang 包下面的很多类,相当一部分都被声明成为final class。在第三方类库的一些基础类中同样如此,这可以有效避免 API 使用者更改基础功能,某种程度上,这是保证平台安全的必要手段。 **final修饰变量** 使用 final 修饰参数或者变量,也可以清楚地避免意外赋值导致的编程错误,甚至,有人明确推荐将所有方法参数、本地变量、成员变量声明成 final。 final 变量产生了某种程度的不可变(immutable)的效果,所以,可以用于保护只读数据,尤其是在并发编程中,因为明确地不能再赋值 final 变量,有利于减少额外的同步开销,也可以省去一些防御性拷贝的必要。 #### finally 关于finally块详细作用,可以查看[专案-浅谈Java异常] 在以下几种特殊情况下,finally代码块不会被执行: - 1.在finally语句块中发生了异常。 - 2.在前面的代码中用了System.exit()退出程序。 - 3.程序所在的线程死亡。 - 4.关闭CPU。 ### String字符串 String、StringBuffer、StringBuilder #### String的创建机制 由于String在Java世界中使用过于频繁,Java为了避免在一个系统中产生大量的String对象,引入了字符串常量池。其运行机制是:创建一个字符串时,首先检查池中是否有值相同的字符串对象,如果有则不需要创建直接从池中刚查找到的对象引用;如果没有则新建字符串对象,返回对象引用,并且将新创建的对象放入池中。但是,通过new方法创建的String对象是不检查字符串池的,而是直接在堆区或栈区创建一个新的对象,也不会把对象放入池中。上述原则只适用于通过直接量给String对象引用赋值的情况。 举例: ```java String str1 = "123"; //通过直接量赋值方式,放入字符串常量池 String str2 = new String(“123”);//通过new方式赋值方式,不放入字符串常量池 ``` 区别: * 通过String a=""直接赋值的方式得到的是一个字符串常量,存在于常量池;注意,相同内容的字符串在常量池中只有一个,即如果池已包含内容相等的字符串会返回池中的字符串,反之会将该字符串放入池中。 * 通过new String("")创建的字符串不是常量是实例对象,会在堆内存开辟空间并存放数据,且每个实例对象都有自己的地址空间 #### String的不可变性 这里主要回答这几个问题: * 为什么Java中的 String 是不可变的(Immutable)? * 字符串设计和实现考量? * String不可变的好处? **不可变类String的原因** * String主要的三个成员变量 char value[], int offset, int count均是private,final的,并且没有对应的 getter/setter; * String 对象一旦初始化完成,上述三个成员变量就不可修改;并且其所提供的接口任何对这些域的修改都将返回一个新对象; * 是典型的 Immutable 类,被声明成为 final class,所有属性也都是final的。也由于它的不可变,类似拼接、裁剪字符串等动作,都会产生新的 String 对象。 **字符串设计和实现考量?** * String 是 Immutable 类的典型实现,原生的保证了基础线程安全,因为你无法对它内部数据进行任何修改,这种便利甚至体现在拷贝构造函数中,由于不可变,Immutable 对象在拷贝时不需要额外复制数据。 * 为了实现修改字符序列的目的,StringBuffer 和 StringBuilder 底层都是利用可修改的(char,JDK 9 以后是 byte)数组,二者都继承了 AbstractStringBuilder,里面包含了基本操作,区别仅在于最终的方法是否加了 synchronized。 * 这个内部数组应该创建成多大的呢?如果太小,拼接的时候可能要重新创建足够大的数组;如果太大,又会浪费空间。目前的实现是,构建时初始字符串长度加 16(这意味着,如果没有构建对象时输入最初的字符串,那么初始值就是 16)。我们如果确定拼接会发生非常多次,而且大概是可预计的,那么就可以指定合适的大小,避免很多次扩容的开销。扩容会产生多重开销,因为要抛弃原有数组,创建新的(可以简单认为是倍数)数组,还要进行arraycopy。 **String不可变的好处?** * **可以缓存 hash 值** 因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。 * **String Pool 的需要** 如果一个String对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。 * **安全性(一致性)** String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 对象的那一方以为现在连接的是其它主机,而实际情况却不一定是。 * **线程安全** String 不可变性天生具备线程安全,可以在多个线程中安全地使用。 #### String演进历史 - String 在 Java 6 以后提供了 intern()方法,目的是提示 JVM 把相应字符串缓存起来,以备重复使用。在我们创建字符串对象并调用 intern() 方法的时候,如果已经有缓存的字符串,就会返回缓存里的实例,否则将其缓存起来。 - 在后续版本中,这个缓存被放置在堆中,这样就极大避免了永久代占满的问题,甚至永久代在 JDK 8 中被 MetaSpace(元数据区)替代了。而且,默认缓存大小也在不断地扩大中,从最初的 1009,到 7u40 以后被修改为 60013。 - Java9中,String实现由巨大变化 todo #### String、StringBuffer、StringBuilder的区别 区别: **可变性** 简单的来说:,String是字符串常量,而StringBuffer、StringBuilder都是字符串变量,即String对象一创建后不可更改,而后两者的对象是可更改的。 String 类中使用 final 关键字字符数组保存字符串,`private final char value[]`,所以 String 对象是不可变的。而StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串`char[]value`但是没有用 final 关键字修饰,所以这两种对象都是可变的。 StringBuilder 与 StringBuffer 的构造方法都是调用父类构造方法也就是 AbstractStringBuilder 实现的,大家可以自行查阅源码。 AbstractStringBuilder.java ~~~java abstract class AbstractStringBuilder implements Appendable, CharSequence { char[] value; int count; AbstractStringBuilder() { } AbstractStringBuilder(int capacity) { value = new char[capacity]; } ~~~ **线程安全性** String 中的对象是final不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。 **性能** 每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。 **对于三者使用的总结:** 1. 操作少量的数据 = String 2. 单线程操作字符串缓冲区下操作大量数据 = StringBuilder 3. 多线程操作字符串缓冲区下操作大量数据 = StringBuffer #### substring的原理及区别 #### replaceFirst、replaceAll、replace 区别 #### String 对“+”的重载、字符串拼接的几种方式和区别 #### String.valueOf 和 Integer.toString 的区别 #### switch 对 String 的支持 ### static #### static关键字可以修饰什么? 可以用来修饰:成员变量,成员方法,代码块,内部类等。具体如下所示: * **修饰成员变量和成员方法** * 被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。 * 被 static 声明的成员变量属于静态成员变量,静态变量存放在Java内存区域的方法区 * **静态代码块** * 静态代码块定义在类中方法外,静态代码块在非静态代码块之前执行(静态代码块—>非静态代码块—>构造方法) * 该类不管创建多少对象,静态代码块只执行一次 * **静态内部类(static修饰类的话只能修饰内部类)** * **静态导包(用来导入类中的静态资源,1.5之后的新特性)** * 这两个关键字连用可以指定导入某个类中的指定静态资源,并且不需要使用类名调用类中静态成员,可以直接使用类中静态成员变量和成员方法。 #### static关键字的特点 静态内部类与非静态内部类之间存在一个最大的区别:非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围内,但是静态内部类却没有。 没有这个引用就意味着: * 1.它的创建是不需要依赖外围类的创建。 * 2.它不能使用任何外围类的非static成员变量和方法。 此外,还有下面使用区别: * 随着类的加载而加载 * 优先于对象存在 * 被类的所有对象共享 * 可以通过类名调用【静态修饰的内容一般我们称其为:与类相关的,类成员】 * 在静态方法中是没有this关键字的(this指针) #### static编译时有啥不同 todo:待添加 ## 数据结构 ### 常用数据结构简介 #### 这里先用几张图,大致介绍下数据结构: 简单来说 ![image](https://upload-images.jianshu.io/upload_images/4432347-95c1a4cac03f1510.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 具体分类 ![image](https://upload-images.jianshu.io/upload_images/4432347-84cb744b434c0bc3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) Java中的数据结构 ![image](https://upload-images.jianshu.io/upload_images/4432347-99e078e9f90f1366.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) **数据结构说明** | 类型 | 简述 | | ---------- | ------------------------------------------------------------ | | 数组(无序) | 优点:查询快,如果知道索引可以快速地存取 <br />缺点:删除慢,大小固定 | | 数组(有序) | 优点:比无序数组查找快 <br />缺点:删除和插入慢,大小固定 | | 栈 | 优点:提供后进先出的存取方式 <br />缺点:存取其他项很慢 | | 队列 | 优点:提供先进先出的存取方式 <br />缺点:存取其他项都很慢 | | 链表 | 优点:插入快,删除快 <br />缺点:查找慢(一个个节点查) | | 二叉树 | 优点:查找,插入,删除都快(平衡二叉树) <br />缺点:删除算法复杂 | | 红黑树 | 优点:查找,插入,删除都快,树总是平衡的(局部调整) <br />缺点:算法复杂 | | 哈希表 | 优点:如果关键字已知则存取速度极快,插入快 <br />缺点:删除慢,如果不知道关键字则存取很慢,对存储空间使用不充分 | | 堆 | 优点:插入,删除快,对最大数据的项存取很快 <br />缺点:对其他数据项存取很慢 | | 图 | 优点:对现实世界建模 <br />缺点:有些算法慢且复杂 | ### List #### Vector、ArrayList、LinkedList有什么区别 36讲-8 ### Map #### Hashtable、HashMap、TreeMap有什么不同 36讲-9 ### Set #### Set 和 List 区别? #### Set 如何保证元素不重复? ### 其他问题 #### System.arraycopy()、Arrays.copyOf() 先来看看方法定义 **System.arraycopy()** 主要是 从源数组A的指定位置index1开始,复制指定长度的元素,到目标数组的制定位置index2。 ```java //System.arraycopy() FastNative public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length); ``` **Arrays.copyOf()** 根据原数组、传入长度, 系统自动在内部新建一个数组,并返回该数组。 ```java public static <T> T[] copyOf(T[] original, int newLength) {} ``` #### Collection 和 Collections 区别 Arrays.asList 获得的 List 使用时需要注意什么 #### 如何保证集合线程安全? 36讲-10 #### ConcurrentHashMap如何实现高效地线程安全 36讲-10 #### fail-fast 和 fail-safe ## 数据结构(具体) ### SparseArray SparseArray位于android.util包下,是Android 对移动端专门优化的的数据结构,类似于 HashMap,key:int ,value:object(是的,你没看错,key只能是int)。 内部实现上,SpareArray中key 和 value 分别采用int[]和object[]两个数组进行存储。存储 key 的数组是 int 类型,不需要进行装箱操作,提供了速度保障。同理,还有 LongSparseArray, BooleanSparseArray 等,都是用来通过减少装箱操作来节省内存空间的。 因为它内部使用二分查找寻找键,所以其效率不如 HashMap 高;在数据量比较少(不超过1000)的情况下,性能会好过 HashMap,数据量大的时候考虑使用HashMap。 添加元素。先采用二分查找法 找到位置,在执行插入,所以两个数组是按照从小到大进行排序的。 查找元素。进行二分查找,数据量少的情况下,速度比较快。 关于SpareArray更详细的分析,可以查看[浅谈SpareArray]() ### HashMap hashmap 如何 put 数据(从 hashmap 源码角度讲解)? (掌握 put 元素的逻辑) 可以参考这里的简单写法(https://juejin.im/post/5c6e97e65188256559172b51) ### HashSet ### TreeMap ## 枚举 ### 枚举的用法、枚举的实现、枚举与单例、Enum 类 ### Java 枚举如何比较 ### switch 对枚举的支持 ### 枚举的序列化如何实现 ### 枚举的线程安全性问题 ## 异常 ### 1、Exception和Error有什么区别 36讲-2 ### 2、try-catch应该包括大段代码吗 这里会尝试说明编译期对 try-catch代码块的处理,才能说明 ### 3、如何合理、高效的使用自定义异常 ## 反射 ## IO ### Java中IO体系 Java IO流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java Io流的40多个类都是从如下4个抽象类基类中派生出来的。 - **InputStream/Reader**: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。 - **OutputStream/Writer**: 所有输出流的基类,前者是字节输出流,后者是字符输出流。 **IO流的分类** - 按照流的流向分,可以分为输入流和输出流; - 按照操作单元划分,可以划分为字节流和字符流; - 按照流的角色划分为节点流和处理流。 按操作方式分类结构图: ![](http://ww1.sinaimg.cn/large/005Ogmrtly1g0ggoexq4qj30k00u0tab.jpg) 按操作对象分类结构图 ![](http://ww1.sinaimg.cn/large/005Ogmrtly1g0ggqb56c3j30k00ev0tp.jpg) ### 字节流、字符流 #### bit、byte、char区别 | 名称 | 内容 | | :--- | :----------------------------------------------------------- | | bit | Bit是最小的二进制单位 ,是计算机的操作部分 取值0或者1 | | byte | Byte是计算机操作数据的最小单位由8位bit组成 取值(-128-127) | | char | Char是用户的可读写的最小单位,在Java里面由16位bit组成 取值(0-65535) | #### 字节流、字符流区别 **简单点说** * 底层设备永远只接受字节数据。 * 由于字节流操作中文不是特别方便,所以,java就提供了字符流。 * 字符流 = 字节流 + 编码表 **扯淡点说** 把二进制数据数据逐一输出到某个设备中,或者从某个设备中逐一读取一片二进制数据,不管输入输出设备是什么,我们要用统一的方式来完成这些操作,用一种抽象的方式进行描述,这个抽象描述方式起名为IO流,对应的抽象类为OutputStream和InputStream ,不同的实现类就代表不同的输入和输出设备,它们都是针对字节进行操作的。 在应用中,经常要完全是字符的一段文本输出去或读进来,用字节流可以吗?计算机中的一切最终都是二进制的字节形式存在。对于“中国”这些字符,首先要得到其对应的字节,然后将字节写入到输出流。读取时,首先读到的是字节,可是我们要把它显示为字符,我们需要将字节转换成字符。由于这样的需求很广泛,人家专门提供了字符流的包装类。 底层设备永远只接受字节数据,有时候要写字符串到底层设备,需要将字符串转成字节再进行写入。字符流是字节流的包装,字符流则是直接接受字符串,它内部将串转成字节,再写入底层设备,这为我们向IO设别写入或读取字符串提供了一点点方便。 #### 如何选择字节流或者字符流? 字符流是由Java虚拟机将字节转化为2个字节的Unicode字符为单位的字符而成的 - 如果是音频文件、图片、歌曲,就用字节流好点(避免数据丢失) - 如果是关系到中文(文本)的,用字符流好点 #### 缓冲区 缓冲区就是一段特殊的内存区域,很多情况下当程序需要频繁地操作一个资源(如文件或数据库)则性能会很低,所以为了提升性能就可以将一部分数据暂时读写到缓存区,以后直接从此区域中读写数据即可,这样就显著提升了性能。 对于 Java 字符流的操作都是在缓冲区操作的,所以如果我们想在字符流操作中主动将缓冲区刷新到文件则可以使用 flush() 方法操作。 #### 字符流的读写方式 5种写数据方式: ```java public void write(int c) public void write(char[] cbuf) public void write(char[] cbuf,int off,int len) public void write(String str) public void write(String str,int off,int len) ``` 2种读数据方式 ```java public int read() public int read(char[] cbuf) ``` ### IO流中使用到的设计模式? #### IO流中用到哪些模式 Todo 这里需要手动补全下 大概有装饰者模式和适配器模式! 要知道装饰者模式和适配器模式的作用;其次,可以自己举个例子把它的作用生动形象地讲出来;最后,简要说一下要完成这样的功能需要什么样的条件 #### 谈一谈IO流中两种设计模式的区别 **装饰器模式** 装饰器模式,就是动态地给一个对象添加一些额外的职责,来实现对原有功能的扩展。 他会有以下三个特征: 1. 它必须持有一个被装饰的对象(作为成员变量)。 2. 它必须拥有与被装饰对象相同的接口(多态调用、扩展需要)。 3. 它可以给被装饰对象添加额外的功能。 这里以java中IO为例 ```java //把InputStreamReader装饰成BufferedReader来成为具备缓冲能力的Reader。 BufferedReader bufferedReader = new BufferedReader(inputStreamReader); ``` 比如,在io流中,FilterInputStream类就是装饰角色,它实现了InputStream类的所有接口,并持有InputStream的对象实例的引用,BufferedInputStream是具体的装饰器实现者,这个装饰器类的作用就是使得InputStream读取的数据保存在内存中,而提高读取的性能。 **适配器模式** 适配器模式,将一个类的接口转换成客户期望的另一个接口,让原本不兼容的接口可以合作无间。 会有以下几个特征: 1. 适配器对象实现原有接口 2. 适配器对象组合一个实现新接口的对象 3. 对适配器原有接口方法的调用被委托给新接口的实例的特定方法(重写旧接口方法来调用新接口功能) ```java //把FileInputStream文件字节流适配成InputStreamReader字符流来操作文件字符串。 FileInputStream fileInput = new FileInputStream(file); InputStreamReader inputStreamReader = new InputStreamReader(fileInput); ``` 比如,在io流中,InputStreamReader类继承了Reader接口,但要创建它必须在构造函数中传入一个InputStream的实例,InputStreamReader的作用也就是将InputStream适配到Reader。InputStreamReader实现了Reader接口,并且持有了InputStream的引用。这里,适配器就是InputStreamReader类,而源角色就是InputStream代表的实例对象,目标接口就是Reader类 **两者的区别** 在Java IO设计中: * **适配器模式**:主要在于将一个接口转变成另一个接口,它的目的是通过改变接口来达到重复使用的目的; * **装饰器模式**:不是要改变被装饰对象的接口,而是保持原有的接口,但是增强原有对象的功能,或改变原有对象的方法而提高性能。 ### IO流案例 #### 字节流复制 一次读取一个字节 ```java public static void main(String[] args) throws IOException { /** * 复制文本文件: * 读和写 * 分析: * 1: 创建两个对象一个是字节输入流对象,一个是字节输出流对象 * 2: 一次读取一个字节,一次写一个字节 * 3: 释放资源 */ // 创建两个对象一个是字节输入流对象,一个是字节输出流对象 FileInputStream fis = new FileInputStream("FileOutputStreamDemo.java") ; FileOutputStream fos = new FileOutputStream("copyFile.java") ; // 一次读取一个字节,一次写一个字节 // int by = 0 ; // while((by = fis.read()) != -1){ // fos.write(by) ; // } // 一次读取一个字节数组复制文件 byte[] bytes = new byte[1024] ; int len = 0 ; // 作用: 记录读取到的有效的字节个数 while((len = fis.read(bytes)) != -1){ fos.write(bytes, 0, len) ; } // 释放资源 fos.close() ; fis.close() ; } ``` #### BufferedInputStream读取数据 ```java public static void main(String[] args) throws IOException { /** * BufferedInputStream构造方法: * public BufferedInputStream(InputStream in) */ BufferedInputStream bis = new BufferedInputStream(new FileInputStream("e.txt")) ; // 一次读取一个字节 //int by = 0 ; //while((by = bis.read()) != -1){ // System.out.print((char)by); //} // 一次读取一个字节数组 byte[] bytes = new byte[1024] ; int len = 0 ; while((len = bis.read(bytes)) != -1){ System.out.print(new String(bytes , 0 , len)); } // 释放资源 bis.close() ; } ``` #### BufferedOutputStream写出数据 ```java public static void main(String[] args) throws IOException { /** * BufferedOutputStream构造方法: * public BufferedOutputStream(OutputStream out) */ // 创建FileOutputStream对象 //FileOutputStream fos = new FileOutputStream("buf.txt") ; //BufferedOutputStream bof = new BufferedOutputStream(fos) ; BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("buf.txt")) ; // 调用方法 bos.write("哈哈,我来了".getBytes()) ; // 释放资源 bos.close() ; } ``` #### 转换流OutputStreamWriter的使用 ```java /** * 字符输出流: OutputStreamWriter (转换输出流) 字符流通向字节流的桥梁 * 构造方法: * public OutputStreamWriter(OutputStream out): 使用默认字符集 * public OutputStreamWriter(OutputStream out , String charsetName): 使用指定的字符集 * 字符输入流: InputStreamReader (转换输入流) 字节流通向字符流的桥梁 * 构造方法: * public InputStreamReader(InputStream in):使用的默认的编码表(GBK) * public InputStreamReader(InputStream in , String charsetName):使用指定的编码表 */ public class OutputStreamWriterDemo { public static void main(String[] args) throws IOException { // 创建: OutputStreamWriter // 创建: OutputStream的对象 // FileOutputStream fos = new FileOutputStream("a.txt") ; // OutputStreamWriter osw = new OutputStreamWriter(fos) ; // OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("a.txt")) ; // public InputStreamReader(InputStream in , String charsetName) 使用指定的编码表 OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("a.txt") , "UTF-8") ; // 调用方法 osw.write("中国") ; // 释放资源 osw.close() ; } } ``` #### 转换流InputStreamReader的使用 ```java public class InputStreamReaderDemo { public static void main(String[] args) throws IOException { // 创建对象InputStreamReader的对象 // public InputStreamReader(InputStream in): 使用的默认的编码表(GBK) // FileInputStream fis = new FileInputStream("a.txt") ; // InputStreamReader isr = new InputStreamReader(fis) ; // 默认的字符集就是GBK // InputStreamReader isr = new InputStreamReader(new FileInputStream("a.txt")) ; // public InputStreamReader(InputStream in , String charsetName) 使用指定的编码表 InputStreamReader isr = new InputStreamReader(new FileInputStream("a.txt") , "utf-8") ; // 读取数据 int ch = 0 ; while((ch = isr.read()) != -1){ System.out.print((char)ch); } // 释放资源 isr.close() ; } } ``` #### 字符流复制 ```java public static void main(String[] args) throws IOException { // 创建转换输入流对象 InputStreamReader isr = new InputStreamReader(new FileInputStream("OutputStreamWriterDemo.java")) ; // 创建转换输出流对象 OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("copyFile.java")) ; // 复制文件 // 一次读取一个字符复制 // int ch = 0 ; // while((ch = isr.read()) != -1){ // osw.write(ch) ; // } // 一次读取一个字符数组复制文件 char[] chs = new char[1024] ; int len = 0 ; while((len = isr.read(chs)) != -1){ osw.write(chs, 0, len) ; } // 释放资源 osw.close() ; isr.close() ; } ``` #### FileWriter和FileReader复制 ```java public static void main(String[] args) throws IOException { // 创建高效的字符输入流对象 BufferedReader br = new BufferedReader(new FileReader("OutputStreamWriterDemo.java")) ; // 创建高效的字符输出流对象 BufferedWriter bw = new BufferedWriter(new FileWriter("copyFile3.java")) ; // 一次读取一个字符数组复制文件 char[] chs = new char[1024] ; int len = 0; while((len = br.read(chs)) != -1){ bw.write(chs, 0, len) ; } // 释放资源 bw.close() ; br.close() ; } ``` #### 字符缓冲流的特殊功能复制文本 ```java public static void main(String[] args) throws IOException { /** * 需求: 使用高效的字符流中特有的功能复制文本文件 */ // 创建高效的字符输入流对象 BufferedReader br = new BufferedReader(new FileReader("OutputStreamWriterDemo.java")) ; // 高效的字符输出流对象 BufferedWriter bw = new BufferedWriter(new FileWriter("copyFile4.java")) ; // 复制文件 // 一次读取一行复制文件 String line = null ; while((line = br.readLine()) != null) { bw.write(line) ; bw.newLine() ; bw.flush() ; } // 释放资源 bw.close() ; br.close() ; } ``` #### 四种方式复制MP3并测试效率 这里通过四种方式来演示复制效率: - 基本字节流一次读写一个字节 - 基本字节流一次读写一个字节数组 - 高效字节流一次读写一个字节 - 高效字节流一次读写一个字节数组 ```java public class CopyFileDemo { public static void main(String[] args) throws IOException { // 获取开始时间 long startTime = System.currentTimeMillis() ; // 复制文件 copyFile_4() ; // 获取结束时间 long endTime = System.currentTimeMillis() ; // 输出 System.out.println("复制文件使用的时间是:" + (endTime - startTime) + "毫秒"); } /** * 基本流一次读取一个字节复制文件 * @throws IOException */ private static void copyFile_1() throws IOException { // 复制文件使用的时间是:88670毫秒 // 创建对象 FileInputStream fis = new FileInputStream("C:\\a.avi") ; FileOutputStream fos = new FileOutputStream("D:\\a.avi") ; // 一次读取一个字节 int by = 0 ; while((by = fis.read()) != -1){ fos.write(by) ; } // 释放资源 fos.close() ; fis.close() ; } /** * 基本流一次读取一个字节数组复制文件 * @throws IOException */ private static void copyFile_2() throws IOException { // 复制文件使用的时间是:130毫秒 // 创建对象 FileInputStream fis = new FileInputStream("C:\\a.avi") ; FileOutputStream fos = new FileOutputStream("D:\\a.avi") ; // 一次读取一个字节数组 byte[] bytes = new byte[1024] ; int len = 0 ; while((len = fis.read(bytes)) != -1){ fos.write(bytes, 0, len) ; } // 释放资源 fos.close() ; fis.close() ; } /** * 高效流一次读取一个字节复制文件 * @throws IOException */ public static void copyFile_3() throws IOException { // 复制文件使用的时间是:990毫秒 // 创建高效流对象 BufferedInputStream bis = new BufferedInputStream(new FileInputStream("C:\\a.avi")) ; BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("D:\\a.avi")) ; // 一次读取一个字节 int by = 0 ; while((by = bis.read()) != -1){ bos.write(by) ; } // 释放资源 bos.close() ; bis.close() ; } /** * 高效流一次读取一个字节数组赋值文件 * @throws IOException */ public static void copyFile_4() throws IOException { // 复制文件使用的时间是:50毫秒 // 创建高效流对象 BufferedInputStream bis = new BufferedInputStream(new FileInputStream("C:\\a.avi")) ; BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("D:\\a.avi")) ; // 一次读取一个字节数组 byte[] bytes = new byte[1024] ; int len = 0 ; while((len = bis.read(bytes)) != -1){ bos.write(bytes, 0, len) ; } // 释放资源 bos.close() ; bis.close() ; } } ``` #### 把集合中的数据存储到文本文件 ```java public static void main(String[] args) throws IOException { /** * 把ArrayList集合中的数据存储到文本文件中 * 分析: * 1: 创建ArrayList集合对象 * 2: 添加数据 * 3: 创建高效的字符输出流对象 * 4: 遍历集合,获取每一个元素,然后通过流对象写出去 * 5: 释放资源 */ // 创建ArrayList集合对象 ArrayList<String> al = new ArrayList<String>() ; // 添加数据 al.add("西施") ; al.add("貂蝉") ; al.add("杨玉环") ; al.add("王昭君") ; // 创建高效的字符输出流对象 BufferedWriter bw = new BufferedWriter(new FileWriter("names.txt")) ; // 遍历集合,获取每一个元素,然后通过流对象写出去 for(String name : al) { bw.write(name) ; bw.newLine() ; bw.flush() ; } // 释放资源 bw.close() ; } ``` #### 把文本文件中的数据存储到集合中 ```java public static void main(String[] args) throws IOException { /** * 从文本文件中读取数据(每一行为一个字符串数据)到集合中,并遍历集合 * 分析: * 1: 创建高效的字符输入流对象 * 2: 创建集合对象 * 3: 读取文本文件中的数据,将数据存储到集合中 * 4: 释放资源 * 5: 遍历集合 */ // 1: 创建高效的字符输入流对象 BufferedReader br = new BufferedReader(new FileReader("names.txt")) ; // 2: 创建集合对象 ArrayList<String> al = new ArrayList<String>() ; // 3: 读取文本文件中的数据,将数据存储到集合中 String line = null ; // 作用: 用来记录读取到的行数据 while((line = br.readLine()) != null) { al.add(line) ; } // 4: 释放资源 br.close() ; // 5: 遍历集合 for(String name : al) { System.out.println(name); } } ``` #### 随机获取文本文件中的姓名 ```java public static void main(String[] args) throws IOException { // 1: 创建集合对象 ArrayList<String> students = new ArrayList<String> () ; // 2: 创建BufferedReader对象 BufferedReader br = new BufferedReader(new FileReader("students.txt")) ; // 3: 读取数据,把数据存储到集合中 String line = null ; while((line = br.readLine()) != null) { students.add(line) ; } // 4: 释放资源 br.close() ; // 5: 生成一个随机数 Random random = new Random() ; int index = random.nextInt(students.size()); // 6: 把生成的随机数作为集合元素的索引,来获取一个元素 String name = students.get(index) ; // 7: 把获取到的元素打印到控制台 System.out.println(name); } ``` #### 复制单级文件夹 ```java public static void main(String[] args) throws IOException { /** * 需求: 把C:\\course这个文件夹复制到D:\\course盘下 * 分析: * 1: 把C:\\course这个目录封装成一个File对象 * 2: 把D:\\course这个目录封装成一个File对象 * 3: 判断D:\\course是否存在,如果存在就创建一个文件夹 * 4: 获取C:\\course这个目录下所有的文件对应的File数组 * 5: 遍历数组,获取元素进行复制 */ // 把C:\\course这个目录封装成一个File对象 File srcFolder = new File("C:\\course") ; // 把D:\\course这个目录封装成一个File对象 File destFolder = new File("D:\\course") ; // 判断D:\\course是否存在,如果存在就创建一个文件夹 if(!destFolder.exists()){ destFolder.mkdir() ; } // 复制文件夹 IOUtils.copyFolder(srcFolder, destFolder, null) ; } ``` #### 复制指定目录下指定后缀名的文件并修改名称 ```java public static void main(String[] args) throws IOException { /** * 把C:\\demo这个目录下所有的以.java结尾的文件复制到D:\\demo中,然后将这个文件的后缀名更改为.jad */ // 把C:\\demo这个目录下所有的以.java结尾的文件复制到D:\\demo中 // 1: 把C:\\demo这个目录封装成一个File对象 File srcFolder = new File("C:\\demo") ; // 2: 把D:\\demo这么目录封装成一个File对象 File destFolder = new File("D:\\demo") ; // 3: 判断D:\\demo这个路径是否存在 if(!destFolder.exists()) { destFolder.mkdir() ; } // 调用方法 IOUtils.copyFolder(srcFolder, destFolder, new FilenameFilter() { @Override public boolean accept(File dir, String name) { return new File(dir , name).isFile() && name.endsWith(".java") ; } }) ; System.out.println("-----------------------------------------------------"); // 获取destFolder下所有的文件对应的File数组 File[] files = destFolder.listFiles() ; for(File f : files) { // 创建目标文件名称 String destFileName = f.getName().replace(".java", ".jad") ; // 创建目标文件 File destFile = new File(destFolder , destFileName) ; // 调用 f.renameTo(destFile) ; } } ``` 上面两步用到的工具类 ```java public class IOUtils { public static void copyFolder(String srcPahtName , String destPathName , FilenameFilter filenameFilter) throws IOException { File srcFolder = new File(srcPahtName) ; File destFolder = new File(destPathName) ; if(!destFolder.exists()) { destFolder.mkdir() ; } copyFolder(srcFolder , destFolder , filenameFilter) ; } public static void copyFolder(File srcFolder , File destFolder , FilenameFilter filenameFilter) throws IOException { File[] files = null ; if(filenameFilter == null) { files = srcFolder.listFiles() ; }else { files = srcFolder.listFiles(filenameFilter) ; } // 遍历 for(File f : files) { // 创建目标文件 String destFileName = f.getName() ; File destFile = new File(destFolder , destFileName) ; // 复制文件 copyFile(f , destFile) ; } } public static void copyFile(File srcFile , File destFile) throws IOException { // 创建流对象 BufferedInputStream bis = new BufferedInputStream(new FileInputStream(srcFile)) ; BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destFile)) ; // 一次读取一个字节数组复制文件 byte[] bytes = new byte[1024] ; int len = 0 ; while((len = bis.read(bytes)) != -1){ bos.write(bytes, 0, len) ; } // 释放资源 bos.close() ; bis.close() ; } } ``` #### 键盘录入学生信息按照总分排序并写入文本文件 ```java public static void main(String[] args) throws IOException { /** * 需求:键盘录入3个学生信息(姓名,语文成绩(chineseScore),数学成绩(mathScore),英语成绩(englishScore)),按照总分从高到低存入文本文件 * 分析: * 1: 创建一个学生类 * 2: 创建一个集合对象TreeSet集合 * 3: 键盘录入学生信息,把学生信息封装到学生对象中,然后把学生对象添加到集合中 * 4: 创建一个高效的字符输出流对象 * 5: 遍历集合,获取每一个元素,把其信息写入到文件中 * 6: 释放资源 */ // 创建一个集合对象TreeSet集合 TreeSet<Student> ts = new TreeSet<Student>(new Comparator<Student>() { @Override public int compare(Student s1, Student s2) { // 总分 int num = s2.getTotalScore() - s1.getTotalScore() ; // 比较姓名 int num2 = (num == 0) ? s2.getName().compareTo(s1.getName()) : num ; // 返回 return num2; } }) ; // 3: 键盘录入学生信息,把学生信息封装到学生对象中,然后把学生对象添加到集合中 for(int x = 1 ; x <= 3 ; x++) { // 创建Scanner对象 Scanner sc = new Scanner(System.in) ; System.out.println("请您输入第" + x + "个学生的姓名" ); String sutName = sc.nextLine() ; System.out.println("请您输入第" + x + "个学生的语文成绩" ); int chineseScore = sc.nextInt() ; System.out.println("请您输入第" + x + "个学生的数学成绩" ); int mathScore = sc.nextInt() ; System.out.println("请您输入第" + x + "个学生的英语成绩" ); int englishScore = sc.nextInt() ; // 把学生的信封装到一个学生对象中 Student s = new Student() ; s.setName(sutName) ; s.setMathScore(mathScore) ; s.setChineseScore(chineseScore) ; s.setEnglishScore(englishScore) ; // 把学生的信息添加到集合中 ts.add(s) ; } // 创建一个高效的字符输出流对象 BufferedWriter bw = new BufferedWriter(new FileWriter("student.info")) ; bw.write("==========================================学生的信息如下====================================================") ; bw.newLine() ; bw.flush() ; bw.write("姓名\t\t总分\t\t数学成绩\t\t语文成绩\t\t英语成绩\t\t") ; bw.newLine() ; bw.flush() ; for(Student t : ts) { bw.write(t.getName() + "\t\t" + t.getTotalScore() + "\t\t" + t.getMathScore() + "\t\t" + t.getChineseScore() + "\t\t" + t.getEnglishScore()) ; bw.newLine() ; bw.flush() ; } // 释放资源 bw.close() ; } ``` ### java提供了那些IO方式? 36讲-11 ### Java中IO与NIO #### IO与NIO简述 传统的IO流是阻塞式的,会一直监听一个ServerSocket,在调用read等方法时,它会一直等到数据到来或者缓冲区已满时才返回。调用accept也是一直阻塞到有客户端连接才会返回。每个客户端连接过来后,服务端都会启动一个线程去处理该客户端的请求。并且多线程处理多个连接。每个线程拥有自己的栈空间并且占用一些CPU时间。每个线程遇到外部未准备好的时候,都会阻塞掉。阻塞的结果就是会带来大量的进程上下文切换。 而对于NIO,它是非阻塞式,核心类: 1. Buffer为所有的原始类型提供 (Buffer)缓存支持。 2. Charset字符集编码解码解决方案 3. Channel一个新的原始I/O抽象,用于读写Buffer类型,通道可以认为是一种连接,可以是到特定设备,程序或者是网络的连接。 #### IO与NIO的主要区别 | IO | NIO | | :----- | :------- | | 面向流 | 面向缓冲 | | 阻塞IO | 非阻塞IO | | 无 | 选择器 | **面向流与面向缓冲** Java IO和NIO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。JavaIO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。JavaNIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。 **阻塞与非阻塞IO** Java IO的各种流是阻塞的。这意味着,当一个线程调用read()或write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。JavaNIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel) **选择器** Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。 ### NIO如何实现多路复用 36讲-11 ### Java有几种文件拷贝方式?那一种最高效 36讲-12 ## JVM ### 字节码 #### 什么是字节码 采用字节码的最大好处是什么 先看下 java 中的编译器和解释器: Java 中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟的机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。 编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在 Java 中,这种供虚拟机理解的代码叫做`字节码`(即扩展名为`.class`的文件),它不面向任何特定的处理器,只面向虚拟机。 每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java 源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行。这也就是解释了 Java 的编译与解释并存的特点。 Java 源代码---->编译器---->jvm 可执行的 Java 字节码(即虚拟指令)---->jvm---->jvm 中解释器----->机器可执行的二进制机器码---->程序运行。 - 采用字节码的好处 **Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行。** > 解释型语言:解释型语言,是在运行的时候将程序翻译成机器语言。解释型语言的程序不需要在运行前编译,在运行程序的时候才翻译,专门的解释器负责在每个语句执行的时候解释程序代码。这样解释型语言每执行一次就要翻译一次,效率比较低。——百度百科 #### Java对象模型 ### JVM内存结构、内存模型、对象模型简介 这里先说几篇文章 - [JVM内存结构 VS Java内存模型 VS Java对象模型](https://mp.weixin.qq.com/s/mJVkLn2I1O7V8jvxc_Z4zw) - [再有人问你Java内存模型是什么,就把这篇文章发给他](https://mp.weixin.qq.com/s/ME_rVwhstQ7FGLPVcfpugQ) - [再有人问你synchronized是什么,就把这篇文章发给他](https://mp.weixin.qq.com/s/tI_4nCIg1kkcf6_UW1aA5A) - [再有人问你volatile是什么,就把这篇文章发给他](https://mp.weixin.qq.com/s/jSDAHKHWogeNU41ZS-fUwA) - [再有人问你volatile是什么,把这篇文章也发给他](https://mp.weixin.qq.com/s/aOQUnuf2_V_XehOxi2FdSQ) JVM内存结构、Java内存模型、Java对象模型 Java作为一种面向对象的,跨平台语言,其对象、内存等一直是比较难的知识点。而且很多概念的名称看起来又那么相似,很多人会傻傻分不清楚。比如本文我们要讨论的**JVM内存结构**、**Java内存模型**和**Java对象模型**,这就是三个截然不同的概念,但是很多人容易弄混。 我们先区分下JVM内存结构、 Java内存模型 以及 Java对象模型 三个概念。 - **JVM内存结构**,和Java虚拟机的运行时区域有关。 - **Java内存模型**,和Java的并发编程有关。 - **Java对象模型**,和Java对象在虚拟机中的表现形式有关。 #### JVM内存结构 我们都知道,Java代码是要运行在虚拟机上的,而虚拟机在执行Java程序的过程中会把所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途。 其中有些区域随着虚拟机进程的启动而存在,而有些区域则依赖用户线程的启动和结束而建立和销毁。在《Java虚拟机规范(Java SE 8)》中描述了JVM运行时内存区域结构如下: ![](http://ww1.sinaimg.cn/large/005Ogmrtly1g0hukgabb0j30ir09qjs1.jpg) 各个区域的功能不是本文重点,就不在这里详细介绍了。这里简单提几个需要特别注意的点: 1、以上是Java虚拟机规范,不同的虚拟机实现会各有不同,但是一般会遵守规范。 2、规范中定义的方法区,只是一种概念上的区域,并说明了其应该具有什么功能。但是并没有规定这个区域到底应该处于何处。所以,对于不同的虚拟机实现来说,是有一定的自由度的。 3、不同版本的方法区所处位置不同,上图中划分的是逻辑区域,并不是绝对意义上的物理区域。因为某些版本的JDK中方法区其实是在堆中实现的。 4、运行时常量池用于存放编译期生成的各种字面量和符号应用。但是,Java语言并不要求常量只有在编译期才能产生。比如在运行期,String.intern也会把新的常量放入池中。 5、除了以上介绍的JVM运行时内存外,还有一块内存区域可供使用,那就是直接内存。Java虚拟机规范并没有定义这块内存区域,所以他并不由JVM管理,是利用本地方法库直接在堆外申请的内存区域。 6、堆和栈的数据划分也不是绝对的,如HotSpot的JIT会针对对象分配做相应的优化。 如上,做个总结,JVM内存结构,由Java虚拟机规范定义。描述的是Java程序执行过程中,由JVM管理的不同数据区域。各个区域有其特定的功能。 #### Java内存模型 Java内存模型看上去和Java内存结构(JVM内存结构)差不多,很多人会误以为两者是一回事儿,这也就导致面试过程中经常答非所为。 在前面的关于JVM的内存结构的图中,我们可以看到,其中Java堆和方法区的区域是多个线程共享的数据区域。也就是说,多个线程可能可以操作保存在堆或者方法区中的同一个数据。这也就是我们常说的“Java的线程间通过共享内存进行通信”。 Java内存模型是根据英文Java Memory Model(JMM)翻译过来的。其实JMM并不像JVM内存结构一样是真实存在的。他只是一个抽象的概念。JSR-133: Java Memory Model and Thread Specification 中描述了,JMM是和多线程相关的,他描述了一组规则或规范,这个规范定义了一个线程对共享变量的写入时对另一个线程是可见的。 那么,简单总结下,Java的多线程之间是通过共享内存进行通信的,而由于采用共享内存进行通信,在通信过程中会存在一系列如可见性、原子性、顺序性等问题,而JMM就是围绕着多线程通信以及与其相关的一系列特性而建立的模型。JMM定义了一些语法集,这些语法集映射到Java语言中就是volatile、synchronized等关键字。 在JMM中,我们把多个线程间通信的共享内存称之为主内存,而在并发编程中多个线程都维护了一个自己的本地内存(这是个抽象概念),其中保存的数据是主内存中的数据拷贝。而JMM主要是控制本地内存和主内存之间的数据交互的。 ![](http://ww1.sinaimg.cn/large/005Ogmrtly1g0hummkjv6j30br0aha9y.jpg) 在Java中,JMM是一个非常重要的概念,正是由于有了JMM,Java的并发编程才能避免很多问题。这里就不对Java内存模型做更加详细的介绍了,想了解更多的朋友可以参考《Java并发编程的艺术》。 #### Java对象模型 Java是一种面向对象的语言,而Java对象在JVM中的存储也是有一定的结构的。而这个关于Java对象自身的存储模型称之为Java对象模型。 HotSpot虚拟机中,设计了一个OOP-Klass Model。OOP(Ordinary Object Pointer)指的是普通对象指针,而Klass用来描述对象实例的具体类型。 每一个Java类,在被JVM加载的时候,JVM会给这个类创建一个`instanceKlass`,保存在方法区,用来在JVM层表示该Java类。当我们在Java代码中,使用new创建一个对象的时候,JVM会创建一个`instanceOopDesc`对象,这个对象中包含了对象头以及实例数据。 ```java class Model{ public static int a = 1; public int b; public Model(int b) { this.b = b; } } public static void main(String[] args) { int c = 10; Model modelA = new Model(2); Model modelB = new Model(3); } ``` ![](http://ww1.sinaimg.cn/large/005Ogmrtly1g0huo0t3v0j30u00dg0u1.jpg) ### Java对象模型 > 关于java对象模型更详细的描述,可以查看[2_Java的对象模型][ref_2_Java的对象模型] m #### 36讲-29 ### 对象内存图 关于java对象模型更详细的资料可以查看[][] **创建对象过程** - (1):加载A.class文件进内存 - (2):在栈内存为s开辟空间 - (3):在堆内存为学生对象开辟空间 - (4):对学生对象的成员变量进行默认初始化 - (5):对学生对象的成员变量进行显示初始化 - (6):通过构造方法对学生对象的成员变量赋值 - (7):学生对象初始化完毕,把对象地址赋值给s变量 **一个对象的内存图** ![](http://ww1.sinaimg.cn/large/005Ogmrtly1g0gi55vtqpj30xp0fg74b.jpg) **两个对象的内存图** ![](http://ww1.sinaimg.cn/large/005Ogmrtly1g0gi5rtkasj30yg0fw3z5.jpg) **三个对象的内存图** ![](http://ww1.sinaimg.cn/large/005Ogmrtly1g0gi5y2j34j30yg0fw3z0.jpg) #### 谈谈JVM内存区域的划分,哪些区域可能发生OutOfMemoryError 36讲-25 ### java中的引用 #### 引用区别:强引用、软引用、弱引用、幻象引用 36讲-4 ### 垃圾回收 #### GC机制浅谈 #### 谈谈GC调优思路 36讲-28 #### Java常见的垃圾收集器有哪些? 36讲-27 ### 编译优化 #### JVM优化java代码时都做了什么 36讲-35 #### 有人说“Lambda能让java程序慢30倍”,你怎么看? 36讲-34 ### 类加载 #### 请介绍类加载过程,什么是双亲委派模型? 36讲-23 ## 多线程 ### 线程基础 #### 进程与线程 **什么是进程?** 进程就是正在运行的程序,是系统进行资源分配和调用的独立单位。每一个进程都有它自己的内存空间和系统资源。 **多进程的意义** 单进程计算机只能做一件事情。而我们现在的计算机都可以一边玩游戏(游戏进程),一边听音乐(音乐进程),所以我们常见的操作系统都是多进程操作系统。比如:Windows,Mac和Linux等,能在同一个时间段内执行多个任务。 对于单核计算机来讲,游戏进程和音乐进程是同时运行的吗?不是。 因为CPU在某个时间点上只能做一件事情,计算机是在游戏进程和音乐进程间做着频繁切换,且切换速度很快, 所以,我们感觉游戏和音乐在同时进行,其实并不是同时执行的。 多进程的作用不是提高执行速度,而是提高CPU的使用率(CPU的处理速度远快于存储的速度)。 **什么是线程** 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。 线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程包含以下内容: 一个指向当前被执行指令的指令指针; 一个栈; 一个寄存器值的集合,定义了一部分描述正在执行线程的处理器状态的值 一个私有的数据区 **多线程有什么意义** 多线程的作用不是提高执行速度,而是为了提高应用程序的使用率。 那么怎么理解这个问题呢? 我们程序在运行的使用,都是在抢CPU的时间片(执行权),如果是多线程的程序,那么在抢到CPU的执行权的概率应该比较单线程程序抢到的概率要大.那么也就是说,CPU在多线程程序中执行的时间要比单线程多,所以就提高了程序的使用率.但是即使是多线程程序,那么他们中的哪个线程能抢占到CPU的资源呢,这个是不确定的,所以多线程具有随机性。 **并行和并发** **并行**是**逻辑**上同时发生,指在某一个时间**内**,同时运行多个程序。 **并发**是**物理**上同时发生,指在某一个时间**点**,同时运行多个程序。 #### 线程与JVM **Java程序运行原理** Java命令会启动java虚拟机,启动JVM,等于启动了一个应用程序,也就是启动了一个进程。 该进程会自动启动一个 “主线程” ,然后主线程去调用某个类的 main 方法。所以 main方法运行在主线程中。 **JVM的启动是多线程的吗** JVM启动至少启动了垃圾回收线程和主线程,所以是多线程的。 #### 线程实现两种方式 java提供了两种方式来实现多线程Thread和Runnable。 **继承Thread** 第一种方式 继承Thread的步骤: 1: 定义一个类,让该类去继承Thread类 2: 重写run方法 3: 创建该类的对象 4: 启动线程 ```java public class ThreadDemo { public static void main(String[] args) { // 创建对象 MyThread t1 = new MyThread() ; MyThread t2 = new MyThread() ; // 启动线程: 需要使用start方法启动线程, 如果我们在这里调用的是run方法,那么我们只是把该方法作为普通方法进行执行 // t1.run() ; // t1.run() ; t1.start() ; // 告诉jvm开启一个线程调用run方法 // t1.start() ; // 一个线程只能被启动一次 t2.start() ; } } public class MyThread extends Thread { @Override public void run() { for(int x = 0 ; x < 1000 ; x++) { System.out.println(x); } } } ``` **实现接口Runnable** 实现多线程的第二中方式步骤: 1: 定义一个类,让该类去实现Runnable接口 2: 重写run方法 3: 创建定义的类的对象 4: 创建Thread的对象吧第三步创建的对象作为参数传递进来 5: 启动线程 ```java public static void main(String[] args) { // 创建定义的类的对象 MyThread mt = new MyThread() ; // 创建Thread的对象吧第三步创建的对象作为参数传递进来 Thread t1 = new Thread(mt , "张三") ; Thread t2 = new Thread(mt , "李四") ; // 启动线程 t1.start() ; t2.start() ; } public class MyThread implements Runnable { @Override public void run() { for(int x = 0 ; x < 1000 ; x++) { System.out.println(Thread.currentThread().getName() + "---" + x); } } } ``` #### 一些常见问题 **thread、Runnable有什么区别** todo:java不支持多继承 **Runnable、callable有什么不同** todo Runnable和Callable都代表那些要在不同的线程中执行的任务。Runnable从JDK1.0开始就有了,Callable是在JDK1.5增加的。它们的主要区别是Callable的 call() 方法可以返回值和抛出异常,而Runnable的run()方法没有这些功能。Callable可以返回装载有计算结果的Future对象。 Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。 这其实是很有用的一个特性,因为多线程相比单线程更难、更复杂的一个重要原因就是因为多线程充满着未知性,某条线程是否执行了?某条线程执行了多久?某条线程执行的时候我们期望的数据是否已经赋值完毕?无法得知,我们能做的只是等待这条多线程的任务执行完毕而已。而Callable+Future/FutureTask却可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务,真的是非常有用。 **run()、start()两种方式的区别** run()方法只是调用了Thread实例的run()方法而已,它仍然运行在主线程上;而start()方法会开辟一个新的线程,在新的线程上调用run()方法,此时它运行在新的线程上 **为什么要重写run方法** 可以在定义的类中,定义多个方法,而方法中的代码并不是所有的都需要线程来进行执行; 如果我们想让某一个段代码被线程执行,那么我们只需要将那一段代码放在run方法中。那么也就是说run方法中封装的都是要被线程执行的代码 ; run方法中的代码的特点: 封装的都是一些比较耗时的代码 **线程能不能多次启动** 一个线程只能被启动一次 todo:一个线程两次调用start()方法会出现什么情况? 36讲-17 **匿名内部类的方式实现多线程程序** - new Thread(){代码…}.start(); - new Thread(new Runnable(){代码…}).start(); **wait()、sleep()区别** * **所在类**。sleep来自Thread类,和wait来自Object类 * **释放对象锁**。调用sleep()方法的过程中,线程不会释放对象锁。而 调用 wait 方法线程会释放对象锁 * **出让资源**。sleep睡眠后不出让系统资源,wait让出系统资源其他线程可以占用CPU sleep(milliseconds)需要指定一个睡眠时间,时间一到会自动唤醒 #### 线程调度 应用程序在执行的时候都需要依赖于线程去抢占CPU的时间片 , 谁抢占到了CPU的时间片,那么CPU就会执行谁 线程的执行:假如我们的计算机只有一个 CPU,那么 CPU在某一个时刻只能执行一条指令,线程只有得到CPU时间片,也就是使用权,才可以执行指令。 **分时调度模型** 所有线程轮流使用CPU的使用权,平均分配每个线程占用 CPU 的时间片。 **抢占式调度模型** 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些。Java使用的是抢占式调度模型。 #### 线程控制 **1、休眠线程** public static void sleep(long time) ; time表达的意思是休眠的时间 , 单位是毫秒 **2、加入线程** public final void join() 等待该线程执行完毕了以后,其他线程才能再次执行 注意事项: 在线程启动之后,在调用方法 **3、礼让线程** public static void yield(): 暂停当前正在执行的线程对象,并执行其他线程。 **线程礼让的原理**: 暂定当前的线程,然后CPU去执行其他的线程, 这个暂定的时间是相当短暂的; 当我某一个线程暂定完毕以后,其他的线程还没有抢占到cpu的执行权 ; 那么这个是时候当前的线程会和其他的线程再次抢占cpu的执行权; **4、守护线程** public final void setDaemon(boolean on) 将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。 该方法必须在启动线程前调用。 jvm会线程程序中存在的线程类型,如果线程全部是守护线程,那么jvm就停止。 **5、中断线程** public final void stop(): 停止线程的运行 public void interrupt(): 中断线程(这个翻译不太好),查看API可得,当线程调用wait(),sleep(long time)方法的时候处于阻塞状态,可以通过这个方法清除阻塞 #### 线程状态 ![](http://ww1.sinaimg.cn/large/005Ogmrtly1g0m83uwg9ej30i20cct9b.jpg) #### 同步、阻塞 同步、非同步,阻塞非阻塞 ### 线程池 有哪几种?分别有什么特点? 如果提交任务时,线程池队列已满,这时会发生什么?线程调度算法是什么? 36讲-21 #### ThreadPoolExecutor类介绍 ExecutorService是最初的线程池接口,ThreadPoolExecutor类是对线程池的具体实现,它通过构造方法来配置线程池的参数。 ```java public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler); } ``` **参数说明**: * **corePoolSize**:线程池中核心线程的数量,默认情况下,即使核心线程没有任务在执行它也存在的,我们固定一定数量的核心线程且它一直存活这样就避免了一般情况下CPU创建和销毁线程带来的开销。我们如果将ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true,那么闲置的核心线程就会有超时策略,这个时间由keepAliveTime来设定,即keepAliveTime时间内如果核心线程没有回应则该线程就会被终止。allowCoreThreadTimeOut默认为false,核心线程没有超时时间。 * **maximumPoolSize**,线程池中的最大线程数,当任务数量超过最大线程数时其它任务可能就会被阻塞。最大线程数=核心线程+非核心线程。非核心线程只有当核心线程不够用且线程池有空余时才会被创建,执行完任务后非核心线程会被销毁。 * **keepAliveTime**,非核心线程的超时时长,当执行时间超过这个时间时,非核心线程就会被回收。当allowCoreThreadTimeOut设置为true时,此属性也作用在核心线程上。unit,枚举时间单位,TimeUnit。 * **workQueue**,线程池中的任务队列,我们提交给线程池的runnable会被存储在这个对象上。 **遵循的规则** - 当线程池中的核心线程数量未达到最大线程数时,启动一个核心线程去执行任务; - 如果线程池中的核心线程数量达到最大线程数时,那么任务会被插入到任务队列中排队等待执行; - 如果在上一步骤中任务队列已满但是线程池中线程数量未达到限定线程总数,那么启动一个非核心线程来处理任务; - 如果上一步骤中线程数量达到了限定线程总量,那么线程池则拒绝执行该任务,且ThreadPoolExecutor会调用RejectedtionHandler的rejectedExecution方法来通知调用者。 #### 线程池的优点 再说线程池的有点前,我们先说说目前遇到的一些问题: 线程的创建和销毁都需要时间,当有大量的线程创建和销毁时,是非常耗cpu和内存的,这样将直接影响系统的吞吐量,导致性能急剧下降,如果内存资源占用的比较多,还很可能造成OOM;此外,也很容易导致GC频繁的执行,从而发生内存抖动现象,而发生了内存抖动,对于移动端来说,最大的影响就是造成界面卡顿 这里是优点: - 1、线程的创建和销毁由线程池维护,一个线程在完成任务后并不会立即销毁,而是由后续的任务复用这个线程,从而减少线程的创建和销毁,节约系统的开销 - 2、线程池旨在线程的复用,这就可以节约我们用以往的方式创建线程和销毁所消耗的时间,减少线程频繁调度的开销,从而节约系统资源,提高系统吞吐量 - 3、在执行大量异步任务时提高了性能 - 4、Java内置的一套ExecutorService线程池相关的api,可以更方便的控制线程的最大并发数、线程的定时任务、单线程的顺序执行等 #### 线程池的分类 目前线程池分为四类: **FixedThreadPool** 通过Executors的newFixedThreadPool()方法创建,它是个线程数量固定的线程池,该线程池的线程全部为核心线程,它们没有超时机制且排队任务队列无限制,因为全都是核心线程,所以响应较快,且不用担心线程会被回收。 **CachedThreadPool** 通过Executors的newCachedThreadPool()方法来创建,它是一个数量无限多的线程池,它所有的线程都是非核心线程,当有新任务来时如果没有空闲的线程则直接创建新的线程,不会去排队而直接执行,并且超时时间都是60s,所以此线程池适合执行大量耗时小的任务。由于设置了超时时间为60s,所以当线程空闲一定时间时就会被系统回收,所以理论上该线程池不会有占用系统资源的无用线程。 **ScheduledThreadPool** 通过Executors的newScheduledThreadPool()方法来创建,ScheduledThreadPool线程池像是上两种的合体,它有数量固定的核心线程,且有数量无限多的非核心线程,但是它的非核心线程超时时间是0s,所以非核心线程一旦空闲立马就会被回收。这类线程池适合用于执行定时任务和固定周期的重复任务。 **SingleThreadExecutor** 通过Executors的newSingleThreadExecutor()方法来创建,它内部只有一个核心线程,它确保所有任务进来都要排队按顺序执行。它的意义在于,统一所有的外界任务到同一线程中,让调用者可以忽略线程同步问题。 #### 线程池一般用法 **一般方法介绍** shutDown(),关闭线程池,需要执行完已提交的任务; shutDownNow(),关闭线程池,并尝试结束已提交的任务; allowCoreThreadTimeOut(boolen),允许核心线程闲置超时回收; execute(),提交任务无返回值; submit(),提交任务有返回值; **线程创建规则** ThreadPoolExecutor对象初始化时,不创建任何执行线程,当有新任务进来时,才会创建执行线程。构造ThreadPoolExecutor对象时,需要配置该对象的核心线程池大小和最大线程池大小: * 当 目前执行线程的总数小于核心线程大小时,所有新加入的任务,都在新线程中处理。 * 当 目前执行线程的总数大于或等于核心线程时,所有新加入的任务,都放入任务缓存队列中。 * 当 目前执行线程的总数大于或等于核心线程,并且缓存队列已满,同时此时线程总数小于线程池的最大大小,那么创建新线程,加入线程池中,协助处理新的任务。 * 当 所有线程都在执行,线程池大小已经达到上限,并且缓存队列已满时,就rejectHandler拒绝新的任务。 ### 同步 参考文章: * [Java虚拟机是如何执行线程同步的][ref_Java虚拟机是如何执行线程同步的] * [1_Synchronized的实现原理][ref_1_Synchronized的实现原理] * [2_Java的对象模型][ref_2_Java的对象模型] * [3_Java的对象头][ref_3_Java的对象头] * [4_Moniter的实现原理][ref_4_Moniter的实现原理] * [5_Java虚拟机的锁优化技术][ref_5_Java虚拟机的锁优化技术] #### 对象锁和类锁的区别 JVM中有两块内存区域可以被所有线程共享: - 堆。上面存放着所有对象 - 方法区。上面存放着静态变量 那么,如果有多个线程想要同时访问同一个对象或者静态变量,就需要被管控,否则可能出现不可预期的结果。 为了协调多个线程之间的共享数据访问,虚拟机给每个对象和类都分配了一个锁。这个锁就像一个特权,在同一时刻,只有一个线程可以“拥有”这个类或者对象。如果一个线程想要获得某个类或者对象的锁,需要询问虚拟机。当一个线程向虚拟机申请某个类或者对象的锁之后,也许很快或者也许很慢虚拟机可以把锁分配给这个线程,同时这个线程也许永远也无法获得锁。当线程不再需要锁的时候,他再把锁还给虚拟机。这时虚拟机就可以再把锁分配给其他申请锁的线程。 **类锁**其实通过对象锁实现的。因为当虚拟机加载一个类的时候,会会为这个类实例化一个 `java.lang.Class` 对象,当你锁住一个类的时候,其实锁住的是其对应的Class 对象。 #### 同步方法和同步代码块区别 对于同步方法,JVM采用ACC_SYNCHRONIZED标记符来实现同步。 对于同步代码块。JVM采用monitorenter、monitorexit两个指令来实现同步。 在java中,synchronized有两种使用形式,同步方法和同步代码块。 ```java public class SynchronizedTest { public synchronized void doSth(){ System.out.println("Hello World"); } public void doSth1(){ synchronized (SynchronizedTest.class){ System.out.println("Hello World"); } } } ``` 反编译以上代码,结果如下(部分无用信息过滤掉了): ``` public synchronized void doSth(); descriptor: ()V flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String Hello World 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return public void doSth1(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=1 0: ldc #5 // class com/hollis/SynchronizedTest 2: dup 3: astore_1 4: monitorenter 5: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 8: ldc #3 // String Hello World 10: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 13: aload_1 14: monitorexit 15: goto 23 18: astore_2 19: aload_1 20: monitorexit 21: aload_2 22: athrow 23: return ``` 我们可以看到Java编译器为我们生成的字节码。在对于doSth和doSth1的处理上稍有不同。也就是说。JVM对于同步方法和同步代码块的处理方式不同。 对于同步方法,JVM采用ACC_SYNCHRONIZED标记符来实现同步。 对于同步代码块。JVM采用monitorenter、monitorexit两个指令来实现同步。 **同步方法** 方法级的同步是隐式的。同步方法的常量池中会有一个`ACC_SYNCHRONIZED`标志。当某个线程要访问某个方法的时候,会检查是否有`ACC_SYNCHRONIZED`,如果有设置,则需要先获得监视器锁,然后开始执行方法,方法执行之后再释放监视器锁。这时如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。值得注意的是,如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。 **同步代码块** 可以把执行`monitorenter`指令理解为加锁,执行`monitorexit`理解为释放锁。 每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为0,当一个线程获得锁(执行`monitorenter`)后,该计数器自增变为 1 ,当同一个线程再次获得该对象的锁的时候,计数器再次自增。当同一个线程释放锁(执行`monitorexit`指令)的时候,计数器再自减。当计数器为0的时候。锁将被释放,其他线程便可以获得锁。 #### Moniter原理 关于`Synchronize`的实现原理,无论是同步方法还是同步代码块,无论是`ACC_SYNCHRONIZED`还是`monitorenter`、`monitorexit`都是基于`Monitor`实现的。 在多线程访问共享资源的时候,经常会带来可见性和原子性的安全问题。为了解决这类线程安全的问题,Java提供了同步机制、互斥锁机制,这个机制保证了在同一时刻只有一个线程能访问共享资源。这个机制的保障来源于监视锁Monitor,每个对象都拥有自己的监视锁Monitor。 在Java虚拟机(HotSpot)中,Monitor是基于C++实现的,由ObjectMonitor实现的,其主要数据结构如下: ```c++ ObjectMonitor() { _header = NULL; _count = 0; _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; _WaitSet = NULL; _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; } ``` 源码地址:objectMonitor.hpp ObjectMonitor中有几个关键属性: * _owner:指向持有ObjectMonitor对象的线程 * _WaitSet:存放处于wait状态的线程队列 * _EntryList:存放处于等待锁block状态的线程队列 * _recursions:锁的重入次数 * _count:用来记录该线程获取锁的次数 当多个线程同时访问一段同步代码时,首先会进入`_EntryList`队列中,当某个线程获取到对象的monitor后进入`_Owner`区域并把monitor中的`_owner`变量设置为当前线程,同时monitor中的计数器`_count`加1。即获得对象锁。 若持有monitor的线程调用`wait()`方法,将释放当前持有的monitor,`_owner`变量恢复为`null`,`_count`自减1,同时该线程进入`_WaitSet`集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。如下图所示 ![](http://ww1.sinaimg.cn/large/005Ogmrtly1g0m7u92c33j30et08vwf1.jpg) 这里附带一张 线程状态图,便于理解: ![](http://ww1.sinaimg.cn/large/005Ogmrtly1g0m83uwg9ej30i20cct9b.jpg) #### 虚拟机中常见的锁优化 常见的锁优化技术:自旋锁、锁消除、锁粗化。但这些优化仅在Java虚拟机server模式会生效,这里对这些优化原理进行简述,各位在开发的过程中,稍加留意即可。 下面会简要的介绍这几种优化技术。 **自旋锁** synchronize的实现方式中使用Monitor进行加锁,这是一种互斥锁,这种互斥锁在互斥同步上对性能的影响很大,Java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统的帮忙,这就要从用户态转换到内核态,因此状态转换需要花费很多的处理器时间。 Java虚拟机的开发工程师们在分析过大量数据后发现:共享数据的锁定状态一般只会持续很短的一段时间,为了这段时间去挂起和恢复线程其实并不值得。 如果物理机上有多个处理器,可以让多个线程同时执行的话。我们就可以让后面来的线程“稍微等一下”,但是并不放弃处理器的执行时间,看看持有锁的线程会不会很快释放锁。这个“稍微等一下”的过程就是自旋。 由于自旋锁只是将当前线程不停地执行循环体,放弃处理器的执行时间,不进行线程状态的改变,所以响应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用CPU时间。**如果线程竞争不激烈,并且保持锁的时间段。适合使用自旋锁**。 **锁消除** 如果同步块所使用的锁对象通过这种分析被证实只能够被一个线程访问,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。 如下代码: ```java public void f() { Object hollis = new Object(); synchronized(hollis) { System.out.println(hollis); } } ``` 这里,可能有读者会质疑了,代码是程序员自己写的,程序员难道没有能力判断要不要加锁吗?就像以上代码,完全没必要加锁,有经验的开发者一眼就能看的出来的。代码中对`hollis`这个对象进行加锁,但是`hollis`对象的生命周期只在`f()`方法中,并不会被其他线程所访问到,所以在JIT编译阶段就会被优化掉。优化成: ```java public void f() { Object hollis = new Object(); System.out.println(hollis); } ``` 其实道理是这样,但是还是有可能有疏忽,比如我们经常在代码中使用StringBuffer作为局部变量,而StringBuffer中的append是线程安全的,有synchronized修饰的,这种情况开发者可能会忽略。这时候,JIT就可以帮忙优化,进行锁消除。 **锁粗化** 很多人都知道,在代码中,需要加锁的时候,我们提倡尽量减小锁的粒度,这样可以避免不必要的阻塞。 这也是很多人原因是用同步代码块来代替同步方法的原因,因为往往他的粒度会更小一些,这其实是很有道理的。 但是。也要分情况。 如果在一段代码中连续的对同一个对象反复加锁解锁,其实是相对耗费资源的,这种情况可以适当放宽加锁的范围,减少性能消耗。 当JIT发现一系列连续的操作都对同一个对象反复加锁和解锁,甚至加锁操作出现在循环体中的时候,会将加锁同步的范围扩散(粗化)到整个操作序列的外部。 如下列: ```java for(int i=0;i<100000;i++){ synchronized(this){ do(); } ``` 会被粗化: ```java synchronized(this){ for(int i=0;i<100000;i++){ do(); } ``` 这其实和我们要求的减小锁粒度并不冲突。减小锁粒度强调的是不要在银行柜台前做准备工作以及和办理业务无关的事情。而锁粗化建议的是,同一个人,要办理多个业务的时候,可以在同一个窗口一次性办完,而不是多次取号多次办理。 #### synchronized和ReentrantLock有什么区别 36讲-15 #### synchronized底层如何实现?什么是锁的升级、降级? 36讲-16 #### synchonized(this)和synchonized(object)区别?Synchronize作用于方法和静态方法区别? ### 内存模型 #### java内存模型中的happer-before是什么 #### AtomicInteger内存模型 ### volatile ### ThreadLocal ### 生产者消费者模型 ### 死锁 什么情况下Java程序会产生死锁?如何定位、修复 36讲-18 也 可以参考这里[死锁的四个必要条件和解决办法](https://blog.csdn.net/guaiguaihenguai/article/details/80303835) 线程死锁的 4 个条件 当两个线程彼此占有对方需要的资源,同时彼此又无法释放自己占有的资源的时候就发生了死锁。发生死锁需要满足下面四个条件, 1. **互斥**:某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束。(一个筷子只能被一个人拿) 2. **占有且等待**:一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进程释放该资源。(每个人拿了一个筷子还要等其他人放弃筷子) 3. **不可抢占**:别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。(别人手里的筷子你不能去抢) 4. **循环等待**:存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源。(每个人都在等相邻的下一个人放弃自己的筷子) #### 请手写一个死锁 ### 其他问题 #### 为什么wait()方法和notify()/notifyAll()方法要在同步块中被调用 这是JDK强制的,wait()方法和notify()/notifyAll()方法在调用前都必须先获得对象的锁 #### wait()方法和notify()/notifyAll()方法在放弃对象监视器时有什么区别 wait()方法和notify()/notifyAll()方法在放弃对象监视器的时候的区别在于:wait()方法立即释放对象监视器,notify()/notifyAll()方法则会等待线程剩余代码执行完毕才会放弃对象监视器。 #### 你是如何调用 wait()方法的?使用 if 块还是循环 todo: wait() 方法应该在循环调用,因为当线程获取到 CPU 开始执行的时候,其他条件可能还没有满足,所以在处理前,循环检测条件是否满足会更好。 ```java public void doSothing(){ synchronized (obj) { while (condition does not hold) obj.wait(); // (Releases lock, and reacquires on wakeup) ... // Perform action appropriate to condition } } ``` 参见 Effective Java 第 69 条,获取更多关于为什么应该在循环中来调用 wait 方法的内容。 #### Java中如何停止一个线程? Java提供了很丰富的API但没有为停止线程提供API。JDK 1.0本来有一些像stop(), suspend() 和 resume()的控制方法但是由于潜在的死锁威胁因此在后续的JDK版本中他们被弃用了,之后Java API的设计者就没有提供一个兼容且线程安全的方法来停止一个线程。当run() 或者 call() 方法执行完的时候线程会自动结束,如果要手动结束一个线程,你可以用volatile 布尔变量来退出run()方法的循环或者是取消任务来中断线程 #### 一个线程运行时发生异常会怎样 这是我在一次面试中遇到的一个[很刁钻的Java面试题](https://link.juejin.im/?target=http%3A%2F%2Fjava67.blogspot.sg%2F2012%2F09%2Ftop-10-tricky-java-interview-questions-answers.html), 简单的说,如果异常没有被捕获该线程将会停止执行。Thread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时候JVM会使用Thread.getUncaughtExceptionHandler()来查询线程的UncaughtExceptionHandler并将线程和异常作为参数传递给handler的uncaughtException()方法进行处理。 #### 如何在两个线程间共享数据? 你可以通过共享对象来实现这个目的,或者是使用像阻塞队列这样并发的数据结构。这篇教程[《Java线程间通信》](https://link.juejin.im/?target=http%3A%2F%2Fjavarevisited.blogspot.sg%2F2013%2F12%2Finter-thread-communication-in-java-wait-notify-example.html)(涉及到在两个线程间共享对象)用wait和notify方法实现了生产者消费者模型。 #### Java中notify 和 notifyAll有什么区别? #### 如何写代码来解决生产者消费者问题? #### 怎么检测一个线程是否拥有锁? 我一直不知道我们竟然可以检测一个线程是否拥有锁,直到我参加了一次电话面试。在java.lang.Thread中有一个方法叫holdsLock(),它返回true如果当且仅当当前线程拥有某个具体对象的锁。你可以查看[这篇文章](https://link.juejin.im/?target=http%3A%2F%2Fjavarevisited.blogspot.com%2F2010%2F10%2Fhow-to-check-if-thread-has-lock-on.html)了解更多。 #### Java中synchronized 和 ReentrantLock 有什么不同? Java在过去很长一段时间只能通过synchronized关键字来实现互斥,它有一些缺点。比如你不能扩展锁之外的方法或者块边界,尝试获取锁时不能中途取消等。Java 5 通过Lock接口提供了更复杂的控制来解决这些问题。 ReentrantLock 类实现了 Lock,它拥有与 synchronized 相同的并发性和内存语义且它还具有可扩展性。 实现原理是synchronized是通过Monitor,ReentrantLock底层调用的是Unsafe的park方法加锁。 #### Java中Lock接口比synchronized块的优势是什么? #### 有三个线程T1,T2,T3,怎么确保它们按顺序执行? 目的是检测你对”join”方法是否熟悉。这个多线程问题比较简单,可以用join方法实现。 #### 如果你提交任务时,线程池队列已满。会时发会生什么? 这个问题问得很狡猾,许多程序员会认为该任务会阻塞直到线程池队列有空位。事实上如果一个任务不能被调度执行那么ThreadPoolExecutor’s submit()方法将会抛出一个RejectedExecutionException异常 #### 如果同步块内的线程抛出异常会发生什么? 这个问题坑了很多Java程序员,若你能想到锁是否释放这条线索来回答还有点希望答对。无论你的同步块是正常还是异常退出的,里面的线程都会释放锁,所以对比锁接口我更喜欢同步块,因为它不用我花费精力去释放锁,该功能可以在[finally block](https://link.juejin.im/?target=http%3A%2F%2Fjavarevisited.blogspot.com%2F2012%2F11%2Fdifference-between-final-finally-and-finalize-java.html)里释放锁实现 #### 单例模式的双检锁是什么? #### 如何在Java中创建线程安全的Singleton? #### java并发包提供了那些并发工具类? 36讲-19 #### 并发包中的ConcurrentLinkedQueue和LinkedBlockingQueue有什么区别? 36讲-20 ## 杂谈 ### 动态代理 静态代理、动态代理 动态代理和反射的关系 动态代理的几种实现方式 AOP #### 有哪些方法可以在运行时铜带生成一个Java类? 36讲-24 ### 如何写出安全的Java代码? 36讲-32 ### 注解 元注解、自定义注解、Java 中常用注解使用、注解与反射的结合 ## 参考 * [JavaGuide](https://github.com/Snailclimb/JavaGuide) [ref_Java虚拟机是如何执行线程同步的]: https://mp.weixin.qq.com/s/DpXS_v-qmUa-bkcVHu9ASQ [ref_1_Synchronized的实现原理]: https://mp.weixin.qq.com/s/637zy26W_fdeopX5dG8OKQ [ref_2_Java的对象模型]: https://mp.weixin.qq.com/s/mWWey3zngiqi-E40PR9U3A [ref_3_Java的对象头]: https://mp.weixin.qq.com/s/3bfUtmhtRvXMGB04aPAIFA [ref_4_Moniter的实现原理]: https://mp.weixin.qq.com/s/uz4RnLG2Na9SvSBdkPflUg [ref_5_Java虚拟机的锁优化技术]: https://mp.weixin.qq.com/s/VDdsKp0uzmh_7vpD9lpLaA