做网站的技术哪个简单,wordpress主题如何升级,安徽鲁班建设集团网站,天津营销网站建设公司文章目录1. Java基础1.1 为什么Java代码可以实现一次编写、到处运行#xff1f;1.2 一个Java文件里可以有多个类吗#xff08;不含内部类#xff09;#xff1f;1.3 说一说你对Java访问权限的了解1.4 介绍一下Java的数据类型1.5 int类型的数据范围是多少#xff1f;1.6 请…
文章目录1. Java基础1.1 为什么Java代码可以实现一次编写、到处运行1.2 一个Java文件里可以有多个类吗不含内部类1.3 说一说你对Java访问权限的了解1.4 介绍一下Java的数据类型1.5 int类型的数据范围是多少1.6 请介绍全局变量和局部变量的区别1.7 请介绍一下实例变量的默认值1.8 为啥要有包装类1.9 说一说自动装箱、自动拆箱的应用场景1.10 如何对Integer和Double类型判断相等1.11 int和Integer有什么区别二者在做运算时会得到什么结果1.12 说一说你对面向对象的理解面向对象1.13 面向对象的三大特征是什么1.14 封装的目的是什么为什么要有封装1.15 说一说你对多态的理解1.16 Java中的多态是怎么实现的1.17 Java为什么是单继承为什么不能多继承1.18 说一说重写与重载的区别1.19 构造方法能不能重写1.20 介绍一下Object类中的方法1.21 说一说hashCode()和equals()的关系1.22 为什么要重写hashCode()和equals()1.23 和equals()有什么区别String1.24 String类有哪些方法1.25 String可以被继承吗1.26 说一说String和StringBuffer有什么区别1.27 说一说StringBuffer和StringBuilder有什么区别1.28 使用字符串时new和推荐使用哪种方式1.29 说一说你对字符串拼接的理解1.30 两个字符串相加的底层是如何实现的1.31 String a abc; 说一下这个过程会创建什么放在哪里1.32 new String(abc) 是去了哪里仅仅是在堆里面吗1.33 接口和抽象类有什么区别1.34 接口中可以有构造函数吗1.35 谈谈你对面向接口编程的理解异常1.36 遇到过异常吗如何处理1.37 说一说Java的异常机制1.38 请介绍Java的异常接口1.39 finally是无条件执行的吗1.40 在finally中return会发生什么static1.41 说一说你对static关键字的理解1.42 static修饰的类能不能被继承1.43 static和final有什么区别泛型1.44 说一说你对泛型的理解1.45 介绍一下泛型擦除1.46 List? super T和List? extends T有什么区别反射1.47 说一说你对Java反射机制的理解1.48 Java反射在实际项目中有哪些应用场景1.49 说一说Java的四种引用方式2. 集合类2.1 Java中有哪些容器集合类2.2 Java中的容器线程安全和线程不安全的分别有哪些2.3 Map接口有哪些实现类2.4 描述一下Map put的过程2.5 如何得到一个线程安全的Map2.6 HashMap有什么特点2.7 JDK7和JDK8中的HashMap有什么区别2.8 介绍一下HashMap底层的实现原理2.9 介绍一下HashMap的扩容机制2.10 HashMap中的循环链表是如何产生的2.11 HashMap为什么用红黑树而不用B树2.12 HashMap为什么线程不安全2.13 HashMap如何实现线程安全2.14 HashMap是如何解决哈希冲突的2.15 说一说HashMap和HashTable的区别2.16 HashMap与ConcurrentHashMap有什么区别2.17 介绍一下ConcurrentHashMap是怎么实现的2.18 ConcurrentHashMap是怎么分段分组的2.19 说一说你对LinkedHashMap的理解2.20 请介绍LinkedHashMap的底层原理2.21 请介绍TreeMap的底层原理2.22 Map和Set有什么区别2.23 List和Set有什么区别2.24 ArrayList和LinkedList有什么区别2.25 有哪些线程安全的List2.26 介绍一下ArrayList的数据结构2.27 谈谈CopyOnWriteArrayList的原理2.28 说一说TreeSet和HashSet的区别2.29 说一说HashSet的底层结构2.30 BlockingQueue中有哪些方法为什么这样设计2.31 BlockingQueue是怎么实现的2.32 Stream不是IOStream有哪些方法3. IO3.1 介绍一下Java中的IO流3.2 怎么用流打开一个大文件3.4 说说NIO的实现原理3.5 介绍一下Java的序列化与反序列化3.6 Serializable接口为什么需要定义serialVersionUID变量3.7 除了Java自带的序列化之外你还了解哪些序列化工具3.8 如果不用JSON工具该如何实现对实体类的序列化3.7 除了Java自带的序列化之外你还了解哪些序列化工具3.8 如果不用JSON工具该如何实现对实体类的序列化1. Java基础
1.1 为什么Java代码可以实现一次编写、到处运行
参考答案
JVMJava虚拟机是Java跨平台的关键。
在程序运行前Java源代码.java需要经过编译器编译成字节码.class。在程序运行时JVM负责将字节码翻译成特定平台下的机器码并运行也就是说只要在不同的平台上安装对应的JVM就可以运行字节码文件。
同一份Java源代码在不同的平台上运行它不需要做任何的改变并且只需要编译一次。而编译好的字节码是通过JVM这个中间的“桥梁”实现跨平台的JVM是与平台相关的软件它能将统一的字节码翻译成该平台的机器码。
注意事项
编译的结果是生成字节码、不是机器码字节码不能直接运行必须通过JVM翻译成机器码才能运行跨平台的是Java程序、而不是JVMJVM是用C/C开发的软件不同平台下需要安装不同版本的JVM。
1.2 一个Java文件里可以有多个类吗不含内部类
参考答案
一个java文件里可以有多个类但最多只能有一个被public修饰的类如果这个java文件中包含public修饰的类则这个类的名称必须和java文件名一致。
1.3 说一说你对Java访问权限的了解
参考答案
Java语言为我们提供了三种访问修饰符即private、protected、public在使用这些修饰符修饰目标时一共可以形成四种访问权限即private、defalut、protected、public注意在不加任何修饰符时为default访问权限。
在修饰成员变量/成员方法时该成员的四种访问权限的含义如下
private该成员可以被该类内部成员访问defalut该成员可以被该类内部成员访问也可以被同一包下其他的类访问protected该成员可以被该类内部成员访问也可以被同一包下其他的类访问还可以被它的子类访问public该成员可以被任意包下任意类的成员进行访问。
在修饰类时该类只有两种访问权限对应的访问权限的含义如下
defalut该类可以被同一包下其他的类访问public该类可以被任意包下任意的类所访问。
1.4 介绍一下Java的数据类型
参考答案
Java数据类型包括基本数据类型和引用数据类型两大类。
基本数据类型有8个可以分为4个小类分别是整数类型byte/short/int/long、浮点类型float/double、字符类型char、布尔类型boolean。其中4个整数类型中int类型最为常用。2个浮点类型中double最为常用。另外在这8个基本类型当中除了布尔类型之外的其他7个类型都可以看做是数字类型它们相互之间可以进行类型转换。
引用类型就是对一个对象的引用根据引用对象类型的不同可以将引用类型分为3类即数组、类、接口类型。引用类型本质上就是通过指针指向堆中对象所持有的内存空间只是Java语言不再沿用指针这个说法而已。
扩展阅读
对于基本数据类型你需要了解每种类型所占据的内存空间面试官可能会追问这类问题
byte1字节8位数据范围是 -2^7 ~ 2^7-1。short2字节16位数据范围是 -2^15 ~ 2^15-1。int4字节32位数据范围是 -2^31 ~ 2^31-1。long8字节64位数据范围是 -2^63 ~ 2^63-1。float4字节32位数据范围大约是 -3.4*10^38 ~ 3.4*10^38。double8字节64位数据范围大约是 -1.8*10^308 ~ 1.8*10^308。char2字节16位数据范围是 \u0000 ~ \uffff。booleanJava规范没有明确的规定不同的JVM有不同的实现机制。
对于引用数据类型你需要了解JVM的内存分布情况知道引用以及引用对象存放的位置详见JVM部分的题目。
1.5 int类型的数据范围是多少
参考答案
int类型占4字节32位数据范围是 -2^31 ~ 2^31-1。
1.6 请介绍全局变量和局部变量的区别
参考答案
Java中的变量分为成员变量和局部变量它们的区别如下
成员变量
成员变量是在类的范围里定义的变量成员变量有默认初始值未被static修饰的成员变量也叫实例变量它存储于对象所在的堆内存中生命周期与对象相同被static修饰的成员变量也叫类变量它存储于方法区中生命周期与当前类相同。
局部变量
局部变量是在方法里定义的变量局部变量没有默认初始值局部变量存储于栈内存中作用的范围结束变量空间会自动的释放。
注意事项
Java中没有真正的全局变量面试官应该是出于其他语言的习惯说全局变量的他的本意应该是指成员变量。
1.7 请介绍一下实例变量的默认值
参考答案
实例变量若为引用数据类型其默认值一律为null。若为基本数据类型其默认值如下
byte0short0int0long0Lfloat0.0Fdouble0.0char’\u0000’booleanfalse
注意事项
上述默认值规则适用于所有的成员变量所以对于类变量也是适用的。
1.8 为啥要有包装类
参考答案
Java语言是面向对象的语言其设计理念是“一切皆对象”。但8种基本数据类型却出现了例外它们不具备对象的特性。正是为了解决这个问题Java为每个基本数据类型都定义了一个对应的引用类型这就是包装类。
扩展阅读
Java之所以提供8种基本数据类型主要是为了照顾程序员的传统习惯。这8种基本数据类型的确带来了一定的方便性但在某些时候也会受到一些制约。比如所有的引用类型的变量都继承于Object类都可以当做Object类型的变量使用但基本数据类型却不可以。如果某个方法需要Object类型的参数但实际传入的值却是数字的话就需要做特殊的处理了。有了包装类这种问题就可以得以简化。
1.9 说一说自动装箱、自动拆箱的应用场景
参考答案
自动装箱、自动拆箱是JDK1.5提供的功能。
自动装箱可以把一个基本类型的数据直接赋值给对应的包装类型
自动拆箱可以把一个包装类型的对象直接赋值给对应的基本类型
通过自动装箱、自动拆箱功能可以大大简化基本类型变量和包装类对象之间的转换过程。比如某个方法的参数类型为包装类型调用时我们所持有的数据却是基本类型的值则可以不做任何特殊的处理直接将这个基本类型的值传入给方法即可。
1.10 如何对Integer和Double类型判断相等
参考答案
Integer、Double不能直接进行比较这包括
不能用进行直接比较因为它们是不同的数据类型不能转为字符串进行比较因为转为字符串后浮点值带小数点整数值不带这样它们永远都不相等不能使用compareTo方法进行比较虽然它们都有compareTo方法但该方法只能对相同类型进行比较。
整数、浮点类型的包装类都继承于Number类型而Number类型分别定义了将数字转换为byte、short、int、long、float、double的方法。所以可以将Integer、Double先转为转换为相同的基本数据类型如double然后使用进行比较。
示例代码
Integer i 100;
Double d 100.00;
System.out.println(i.doubleValue() d.doubleValue());1.11 int和Integer有什么区别二者在做运算时会得到什么结果
参考答案
int是基本数据类型Integer是int的包装类。二者在做运算时Integer会自动拆箱为int类型然后再进行比较。届时如果两个int值相等则返回true否则就返回false。
1.12 说一说你对面向对象的理解
参考答案
面向对象是一种更优秀的程序设计方法它的基本思想是使用类、对象、继承、封装、消息等基本概念进行程序设计。它从现实世界中客观存在的事物出发来构造软件系统并在系统构造中尽可能运用人类的自然思维方式强调直接以现实世界中的事物为中心来思考认识问题并根据这些事物的本质特点把它们抽象地表示为系统中的类作为系统的基本构成单元这使得软件系统的组件可以直接映像到客观世界并保持客观世界中事物及其相互关系的本来面貌。
扩展阅读
结构化程序设计方法主张按功能来分析系统需求其主要原则可概括为自顶向下、逐步求精、模块化等。结构化程序设计首先采用结构化分析方法对系统进行需求分析然后使用结构化设计方法对系统进行概要设计、详细设计最后采用结构化编程方法来实现系统。
因为结构化程序设计方法主张按功能把软件系统逐步细分因此这种方法也被称为面向功能的程序设计方法结构化程序设计的每个功能都负责对数据进行一次处理每个功能都接受一些数据处理完后输出一些数据这种处理方式也被称为面向数据流的处理方式。
结构化程序设计里最小的程序单元是函数每个函数都负责完成一个功能用以接收一些输入数据函数对这些输入数据进行处理处理结束后输出一些数据。整个软件系统由一个个函数组成其中作为程序入口的函数被称为主函数主函数依次调用其他普通函数普通函数之间依次调用从而完成整个软件系统的功能。
每个函数都是具有输入、输出的子系统函数的输入数据包括函数形参、全局变量和常量等函数的输出数据包括函数返回值以及传出参数等。结构化程序设计方式有如下两个局限性
设计不够直观与人类习惯思维不一致。采用结构化程序分析、设计时开发者需要将客观世界模型分解成一个个功能每个功能用以完成一定的数据处理。适应性差可扩展性不强。由于结构化设计采用自顶向下的设计方式所以当用户的需求发生改变或需要修改现有的实现方式时都需要自顶向下地修改模块结构这种方式的维护成本相当高。
面向对象
1.13 面向对象的三大特征是什么
参考答案
面向对象的程序设计方法具有三个基本特征封装、继承、多态。其中封装指的是将对象的实现细节隐藏起来然后通过一些公用方法来暴露该对象的功能继承是面向对象实现软件复用的重要手段当子类继承父类后子类作为一种特殊的父类将直接获得父类的属性和方法多态指的是子类对象可以直接赋给父类变量但运行时依然表现出子类的行为特征这意味着同一个类型的对象在执行同一个方法时可能表现出多种行为特征。
扩展阅读
抽象也是面向对象的重要部分抽象就是忽略一个主题中与当前目标无关的那些方面以便更充分地注意与当前目标有关的方面。抽象并不打算了解全部问题而只是考虑部分问题。例如需要考察Person对象时不可能在程序中把Person的所有细节都定义出来通常只能定义Person的部分数据、部分行为特征而这些数据、行为特征是软件系统所关心的部分。
1.14 封装的目的是什么为什么要有封装
参考答案
封装是面向对象编程语言对客观世界的模拟在客观世界里对象的状态信息都被隐藏在对象内部外界无法直接操作和修改。对一个类或对象实现良好的封装可以实现以下目的
隐藏类的实现细节让使用者只能通过事先预定的方法来访问数据从而可以在该方法里加入控制逻辑限制对成员变量的不合理访问可进行数据检查从而有利于保证对象信息的完整性便于修改提高代码的可维护性。
扩展阅读
为了实现良好的封装需要从两个方面考虑
将对象的成员变量和实现细节隐藏起来不允许外部直接访问把方法暴露出来让方法来控制对这些成员变量进行安全的访问和操作。
封装实际上有两个方面的含义把该隐藏的隐藏起来把该暴露的暴露出来。这两个方面都需要通过使用Java提供的访问控制符来实现。
1.15 说一说你对多态的理解
参考答案
因为子类其实是一种特殊的父类因此Java允许把一个子类对象直接赋给一个父类引用变量无须任何类型转换或者被称为向上转型向上转型由系统自动完成。
当把一个子类对象直接赋给父类引用变量时例如 BaseClass obj new SubClass();这个obj引用变量的编译时类型是BaseClass而运行时类型是SubClass当运行时调用该引用变量的方法时其方法行为总是表现出子类方法的行为特征而不是父类方法的行为特征这就可能出现相同类型的变量、调用同一个方法时呈现出多种不同的行为特征这就是多态。
扩展阅读
多态可以提高程序的可扩展性在设计程序时让代码更加简洁而优雅。
例如我要设计一个司机类他可以开轿车、巴士、卡车等等示例代码如下
class Driver {void drive(Car car) { ... }void drive(Bus bus) { ... }void drive(Truck truck) { ... }
}在设计上述代码时我已采用了重载机制将方法名进行了统一。这样在进行调用时无论要开什么交通工具都是通过 driver.drive(obj) 这样的方式来调用对调用者足够的友好。
但对于程序的开发者来说这显得繁琐因为实际上这个司机可以驾驶更多的交通工具。当系统需要为这个司机增加车型时开发者就需要相应的增加driver方法类似的代码会堆积的越来越多显得臃肿。
采用多态的方式来设计上述程序就会变得简洁很多。我们可以为所有的交通工具定义一个父类Vehicle然后按照如下的方式设计drive方法。调用时我们可以传入Vehicle类型的实例也可以传入任意的Vechile子类型的实例对于调用者来说一样的方便但对于开发者来说代码却变得十分的简洁了。
class Driver {void drive(Vehicle vehicle) { ... }
}1.16 Java中的多态是怎么实现的
参考答案
多态的实现离不开继承在设计程序时我们可以将参数的类型定义为父类型。在调用程序时则可以根据实际情况传入该父类型的某个子类型的实例这样就实现了多态。对于父类型可以有三种形式即普通的类、抽象类、接口。对于子类型则要根据它自身的特征重写父类的某些方法或实现抽象类/接口的某些抽象方法。
1.17 Java为什么是单继承为什么不能多继承
参考答案
首先Java是单继承的指的是Java中一个类只能有一个直接的父类。Java不能多继承则是说Java中一个类不能直接继承多个父类。
其次Java在设计时借鉴了C的语法而C是支持多继承的。Java语言之所以摒弃了多继承的这项特征是因为多继承容易产生混淆。比如两个父类中包含相同的方法时子类在调用该方法或重写该方法时就会迷惑。
准确来说Java是可以实现多继承的。因为尽管一个类只能有一个直接父类但是却可以有任意多个间接的父类。这样的设计方式避免了多继承时所产生的混淆。
1.18 说一说重写与重载的区别
参考答案
重载发生在同一个类中若多个方法之间方法名相同、参数列表不同则它们构成重载的关系。重载与方法的返回值以及访问修饰符无关即重载的方法不能根据返回类型进行区分。
重写发生在父类子类中若子类方法想要和父类方法构成重写关系则它的方法名、参数列表必须与父类方法相同。另外返回值要小于等于父类方法抛出的异常要小于等于父类方法访问修饰符则要大于等于父类方法。还有若父类方法的访问修饰符为private则子类不能对其重写。
1.19 构造方法能不能重写
参考答案
构造方法不能重写。因为构造方法需要和类保持同名而重写的要求是子类方法要和父类方法保持同名。如果允许重写构造方法的话那么子类中将会存在与类名不同的构造方法这与构造方法的要求是矛盾的。
1.20 介绍一下Object类中的方法
参考答案
Object类提供了如下几个常用方法
Class? getClass()返回该对象的运行时类。boolean equals(Object obj)判断指定对象与该对象是否相等。int hashCode()返回该对象的hashCode值。在默认情况下Object类的hashCode()方法根据该对象的地址来计算。但很多类都重写了Object类的hashCode()方法不再根据地址来计算其hashCode()方法值。String toString()返回该对象的字符串表示当程序使用System.out.println()方法输出一个对象或者把某个对象和字符串进行连接运算时系统会自动调用该对象的toString()方法返回该对象的字符串表示。Object类的toString()方法返回 运行时类名十六进制hashCode值 格式的字符串但很多类都重写了Object类的toString()方法用于返回可以表述该对象信息的字符串。
另外Object类还提供了wait()、notify()、notifyAll()这几个方法通过这几个方法可以控制线程的暂停和运行。Object类还提供了一个clone()方法该方法用于帮助其他对象来实现“自我克隆”所谓“自我克隆”就是得到一个当前对象的副本而且二者之间完全隔离。由于该方法使用了protected修饰因此它只能被子类重写或调用。
扩展阅读
Object类还提供了一个finalize()方法当系统中没有引用变量引用到该对象时垃圾回收器调用此方法来清理该对象的资源。并且针对某一个对象垃圾回收器最多只会调用它的finalize()方法一次。
注意finalize()方法何时调用、是否调用都是不确定的我们也不要主动调用finalize()方法。从JDK9开始这个方法被标记为不推荐使用的方法。
1.21 说一说hashCode()和equals()的关系
参考答案
hashCode()用于获取哈希码散列码eauqls()用于比较两个对象是否相等它们应遵守如下规定
如果两个对象相等则它们必须有相同的哈希码。如果两个对象有相同的哈希码则它们未必相等。
扩展阅读
在Java中Set接口代表无序的、元素不可重复的集合HashSet则是Set接口的典型实现。
当向HashSet中加入一个元素时它需要判断集合中是否已经包含了这个元素从而避免重复存储。由于这个判断十分的频繁所以要讲求效率绝不能采用遍历集合逐个元素进行比较的方式。实际上HashSet是通过获取对象的哈希码以及调用对象的equals()方法来解决这个判断问题的。
HashSet首先会调用对象的hashCode()方法获取其哈希码并通过哈希码确定该对象在集合中存放的位置。假设这个位置之前已经存了一个对象则HashSet会调用equals()对两个对象进行比较。若相等则说明对象重复此时不会保存新加的对象。若不等说明对象不重复但是它们存储的位置发生了碰撞此时HashSet会采用链式结构在同一位置保存多个对象即将新加对象链接到原来对象的之后。之后再有新添加对象也映射到这个位置时就需要与这个位置中所有的对象进行equals()比较若均不相等则将其链到最后一个对象之后。
1.22 为什么要重写hashCode()和equals()
参考答案
Object类提供的equals()方法默认是用来进行比较的也就是说只有两个对象是同一个对象时才能返回相等的结果。而实际的业务中我们通常的需求是若两个不同的对象它们的内容是相同的就认为它们相等。鉴于这种情况Object类中equals()方法的默认实现是没有实用价值的所以通常都要重写。
由于hashCode()与equals()具有联动关系参考“说一说hashCode()和equals()的关系”一题所以equals()方法重写时通常也要将hashCode()进行重写使得这两个方法始终满足相关的约定。
1.23 和equals()有什么区别
参考答案
运算符
作用于基本数据类型时是比较两个数值是否相等作用于引用数据类型时是比较两个对象的内存地址是否相同即判断它们是否为同一个对象
equals()方法
没有重写时Object默认以 来实现即比较两个对象的内存地址是否相同进行重写后一般会按照对象的内容来进行比较若两个对象内容相同则认为对象相等否则认为对象不等。
String
1.24 String类有哪些方法
参考答案
String类是Java最常用的API它包含了大量处理字符串的方法比较常用的有
char charAt(int index)返回指定索引处的字符String substring(int beginIndex, int endIndex)从此字符串中截取出一部分子字符串String[] split(String regex)以指定的规则将此字符串分割成数组String trim()删除字符串前导和后置的空格int indexOf(String str)返回子串在此字符串首次出现的索引int lastIndexOf(String str)返回子串在此字符串最后出现的索引boolean startsWith(String prefix)判断此字符串是否以指定的前缀开头boolean endsWith(String suffix)判断此字符串是否以指定的后缀结尾String toUpperCase()将此字符串中所有的字符大写String toLowerCase()将此字符串中所有的字符小写String replaceFirst(String regex, String replacement)用指定字符串替换第一个匹配的子串String replaceAll(String regex, String replacement)用指定字符串替换所有的匹配的子串。
注意事项
String类的方法太多了你没必要都记下来更不需要一一列举。面试时能说出一些常用的方法表现出对这个类足够的熟悉就可以了。另外建议你挑几个方法仔细看看源码实现面试时可以重点说这几个方法。
1.25 String可以被继承吗
参考答案
String类由final修饰所以不能被继承。
扩展阅读
在Java中String类被设计为不可变类主要表现在它保存字符串的成员变量是final的。
Java 9之前字符串采用char[]数组来保存字符即 private final char[] valueJava 9做了改进采用byte[]数组来保存字符即 private final byte[] value
之所以要把String类设计为不可变类主要是出于安全和性能的考虑可归纳为如下4点。
由于字符串无论在任何 Java 系统中都广泛使用会用来存储敏感信息如账号密码网络路径文件处理等场景里保证字符串 String 类的安全性就尤为重要了如果字符串是可变的容易被篡改那我们就无法保证使用字符串进行操作时它是安全的很有可能出现 SQL 注入访问危险文件等操作。在多线程中只有不变的对象和值是线程安全的可以在多个线程中共享数据。由于 String 天然的不可变当一个线程”修改“了字符串的值只会产生一个新的字符串对象不会对其他线程的访问产生副作用访问的都是同样的字符串数据不需要任何同步操作。字符串作为基础的数据结构大量地应用在一些集合容器之中尤其是一些散列集合在散列集合中存放元素都要根据对象的 hashCode() 方法来确定元素的位置。由于字符串 hashcode 属性不会变更保证了唯一性使得类似 HashMapHashSet 等容器才能实现相应的缓存功能。由于 String 的不可变避免重复计算 hashcode只要使用缓存的 hashcode 即可这样一来大大提高了在散列集合中使用 String 对象的性能。当字符串不可变时字符串常量池才有意义。字符串常量池的出现可以减少创建相同字面量的字符串让不同的引用指向池中同一个字符串为运行时节约很多的堆内存。若字符串可变字符串常量池失去意义基于常量池的 String.intern() 方法也失效每次创建新的字符串将在堆内开辟出新的空间占据更多的内存。
因为要保证String类的不可变那么将这个类定义为final的就很容易理解了。如果没有final修饰那么就会存在String的子类这些子类可以重写String类的方法强行改变字符串的值这便违背了String类设计的初衷。
1.26 说一说String和StringBuffer有什么区别
参考答案
String类是不可变类即一旦一个String对象被创建以后包含在这个对象中的字符序列是不可改变的直至这个对象被销毁。
StringBuffer对象则代表一个字符序列可变的字符串当一个StringBuffer被创建以后通过StringBuffer提供的append()、insert()、reverse()、setCharAt()、setLength()等方法可以改变这个字符串对象的字符序列。一旦通过StringBuffer生成了最终想要的字符串就可以调用它的toString()方法将其转换为一个String对象。
1.27 说一说StringBuffer和StringBuilder有什么区别
参考答案
StringBuffer、StringBuilder都代表可变的字符串对象它们有共同的父类 AbstractStringBuilder并且两个类的构造方法和成员方法也基本相同。不同的是StringBuffer是线程安全的而StringBuilder是非线程安全的所以StringBuilder性能略高。一般情况下要创建一个内容可变的字符串建议优先考虑StringBuilder类。
1.28 使用字符串时new和推荐使用哪种方式
参考答案
先看看 hello 和 new String(hello) 的区别
当Java程序直接使用 hello 的字符串直接量时JVM将会使用常量池来管理这个字符串当使用 new String(hello) 时JVM会先使用常量池来管理 hello 直接量再调用String类的构造器来创建一个新的String对象新创建的String对象被保存在堆内存中。
显然采用new的方式会多创建一个对象出来会占用更多的内存所以一般建议使用直接量的方式创建字符串。
1.29 说一说你对字符串拼接的理解
参考答案
拼接字符串有很多种方式其中最常用的有4种下面列举了这4种方式各自适合的场景。 运算符如果拼接的都是字符串直接量则适合使用 运算符实现拼接StringBuilder如果拼接的字符串中包含变量并不要求线程安全则适合使用StringBuilderStringBuffer如果拼接的字符串中包含变量并且要求线程安全则适合使用StringBufferString类的concat方法如果只是对两个字符串进行拼接并且包含变量则适合使用concat方法
扩展阅读
采用 运算符拼接字符串时
如果拼接的都是字符串直接量则在编译时编译器会将其直接优化为一个完整的字符串和你直接写一个完整的字符串是一样的所以效率非常的高。如果拼接的字符串中包含变量则在编译时编译器采用StringBuilder对其进行优化即自动创建StringBuilder实例并调用其append()方法将这些字符串拼接在一起效率也很高。但如果这个拼接操作是在循环中进行的那么每次循环编译器都会创建一个StringBuilder实例再去拼接字符串相当于执行了 new StringBuilder().append(str)所以此时效率很低。
采用StringBuilder/StringBuffer拼接字符串时
StringBuilder/StringBuffer都有字符串缓冲区缓冲区的容量在创建对象时确定并且默认为16。当拼接的字符串超过缓冲区的容量时会触发缓冲区的扩容机制即缓冲区加倍。缓冲区频繁的扩容会降低拼接的性能所以如果能提前预估最终字符串的长度则建议在创建可变字符串对象时放弃使用默认的容量可以指定缓冲区的容量为预估的字符串的长度。
采用String类的concat方法拼接字符串时
concat方法的拼接逻辑是先创建一个足以容纳待拼接的两个字符串的字节数组然后先后将两个字符串拼到这个数组里最后将此数组转换为字符串。在拼接大量字符串的时候concat方法的效率低于StringBuilder。但是只拼接2个字符串时concat方法的效率要优于StringBuilder。并且这种拼接方式代码简洁所以只拼2个字符串时建议优先选择concat方法。
1.30 两个字符串相加的底层是如何实现的
参考答案
如果拼接的都是字符串直接量则在编译时编译器会将其直接优化为一个完整的字符串和你直接写一个完整的字符串是一样的。
如果拼接的字符串中包含变量则在编译时编译器采用StringBuilder对其进行优化即自动创建StringBuilder实例并调用其append()方法将这些字符串拼接在一起。
1.31 String a “abc”; 说一下这个过程会创建什么放在哪里
参考答案
JVM会使用常量池来管理字符串直接量。在执行这句话时JVM会先检查常量池中是否已经存有abc若没有则将abc存入常量池否则就复用常量池中已有的abc将其引用赋值给变量a。
1.32 new String(“abc”) 是去了哪里仅仅是在堆里面吗
参考答案
在执行这句话时JVM会先使用常量池来管理字符串直接量即将abc存入常量池。然后再创建一个新的String对象这个对象会被保存在堆内存中。并且堆中对象的数据会指向常量池中的直接量。
1.33 接口和抽象类有什么区别
参考答案
从设计目的上来说二者有如下的区别
接口体现的是一种规范。对于接口的实现者而言接口规定了实现者必须向外提供哪些服务对于接口的调用者而言接口规定了调用者可以调用哪些服务以及如何调用这些服务。当在一个程序中使用接口时接口是多个模块间的耦合标准当在多个应用程序之间使用接口时接口是多个程序之间的通信标准。
抽象类体现的是一种模板式设计。抽象类作为多个子类的抽象父类可以被当成系统实现过程中的中间产品这个中间产品已经实现了系统的部分功能但这个产品依然不能当成最终产品必须有更进一步的完善这种完善可能有几种不同方式。
从使用方式上来说二者有如下的区别
接口里只能包含抽象方法、静态方法、默认方法和私有方法不能为普通方法提供方法实现抽象类则完全可以包含普通方法。接口里只能定义静态常量不能定义普通成员变量抽象类里则既可以定义普通成员变量也可以定义静态常量。接口里不包含构造器抽象类里可以包含构造器抽象类里的构造器并不是用于创建对象而是让其子类调用这些构造器来完成属于抽象类的初始化操作。接口里不能包含初始化块但抽象类则完全可以包含初始化块。一个类最多只能有一个直接父类包括抽象类但一个类可以直接实现多个接口通过实现多个接口可以弥补Java单继承的不足。
扩展阅读
接口和抽象类很像它们都具有如下共同的特征
接口和抽象类都不能被实例化它们都位于继承树的顶端用于被其他类实现和继承。接口和抽象类都可以包含抽象方法实现接口或继承抽象类的普通子类都必须实现这些抽象方法。
1.34 接口中可以有构造函数吗
参考答案
由于接口定义的是一种规范因此接口里不能包含构造器和初始化块定义。接口里可以包含成员变量只能是静态常量、方法只能是抽象实例方法、类方法、默认方法或私有方法、内部类包括内部接口、枚举定义。
1.35 谈谈你对面向接口编程的理解
参考答案
接口体现的是一种规范和实现分离的设计哲学充分利用接口可以极好地降低程序各模块之间的耦合从而提高系统的可扩展性和可维护性。基于这种原则很多软件架构设计理论都倡导“面向接口”编程而不是面向实现类编程希望通过面向接口编程来降低程序的耦合。
异常
1.36 遇到过异常吗如何处理
参考答案
在Java中可以按照如下三个步骤处理异常 捕获异常 将业务代码包裹在try块内部当业务代码中发生任何异常时系统都会为此异常创建一个异常对象。创建异常对象之后JVM会在try块之后寻找可以处理它的catch块并将异常对象交给这个catch块处理。 处理异常 在catch块中处理异常时应该先记录日志便于以后追溯这个异常。然后根据异常的类型、结合当前的业务情况进行相应的处理。比如给变量赋予一个默认值、直接返回空值、向外抛出一个新的业务异常交给调用者处理等等。 回收资源 如果业务代码打开了某个资源比如数据库连接、网络连接、磁盘文件等则需要在这段业务代码执行完毕后关闭这项资源。并且无论是否发生异常都要尝试关闭这项资源。将关闭资源的代码写在finally块内可以满足这种需求即无论是否发生异常finally块内的代码总会被执行。
1.37 说一说Java的异常机制
参考答案
关于异常处理
在Java中处理异常的语句由try、catch、finally三部分组成。其中try块用于包裹业务代码catch块用于捕获并处理某个类型的异常finally块则用于回收资源。当业务代码发生异常时系统会创建一个异常对象然后由JVM寻找可以处理这个异常的catch块并将异常对象交给这个catch块处理。若业务代码打开了某项资源则可以在finally块中关闭这项资源因为无论是否发生异常finally块一定会执行。
关于抛出异常
当程序出现错误时系统会自动抛出异常。除此以外Java也允许程序主动抛出异常。当业务代码中判断某项错误的条件成立时可以使用throw关键字向外抛出异常。在这种情况下如果当前方法不知道该如何处理这个异常可以在方法签名上通过throws关键字声明抛出异常则该异常将交给JVM处理。
关于异常跟踪栈
程序运行时经常会发生一系列方法调用从而形成方法调用栈。异常机制会导致异常在这些方法之间传播而异常传播的顺序与方法的调用相反。异常从发生异常的方法向外传播首先传给该方法的调用者再传给上层调用者以此类推。最终会传到main方法若依然没有得到处理则JVM会终止程序并打印异常跟踪栈的信息
1.38 请介绍Java的异常接口
参考答案
Throwable是异常的顶层父类代表所有的非正常情况。它有两个直接子类分别是Error、Exception。
Error是错误一般是指与虚拟机相关的问题如系统崩溃、虚拟机错误、动态链接失败等这种错误无法恢复或不可能捕获将导致应用程序中断。通常应用程序无法处理这些错误因此应用程序不应该试图使用catch块来捕获Error对象。在定义方法时也无须在其throws子句中声明该方法可能抛出Error及其任何子类。
Exception是异常它被分为两大类分别是Checked异常和Runtime异常。所有的RuntimeException类及其子类的实例被称为Runtime异常不是RuntimeException类及其子类的异常实例则被称为Checked异常。Java认为Checked异常都是可以被处理修复的异常所以Java程序必须显式处理Checked异常。如果程序没有处理Checked异常该程序在编译时就会发生错误无法通过编译。Runtime异常则更加灵活Runtime异常无须显式声明抛出如果程序需要捕获Runtime异常也可以使用try…catch块来实现。
1.39 finally是无条件执行的吗
参考答案
不管try块中的代码是否出现异常也不管哪一个catch块被执行甚至在try块或catch块中执行了return语句finally块总会被执行。
注意事项
如果在try块或catch块中使用 System.exit(1); 来退出虚拟机则finally块将失去执行的机会。但是我们在实际的开发中重来都不会这样做所以尽管存在这种导致finally块无法执行的可能也只是一种可能而已。
1.40 在finally中return会发生什么
参考答案
在通常情况下不要在finally块中使用return、throw等导致方法终止的语句一旦在finally块中使用了return、throw语句将会导致try块、catch块中的return、throw语句失效。
详细解析
当Java程序执行try块、catch块时遇到了return或throw语句这两个语句都会导致该方法立即结束但是系统执行这两个语句并不会结束该方法而是去寻找该异常处理流程中是否包含finally块如果没有finally块程序立即执行return或throw语句方法终止如果有finally块系统立即开始执行finally块。只有当finally块执行完成后系统才会再次跳回来执行try块、catch块里的return或throw语句如果finally块里也使用了return或throw等导致方法终止的语句finally块已经终止了方法系统将不会跳回去执行try块、catch块里的任何代码。
static
1.41 说一说你对static关键字的理解
参考答案
在Java类里只能包含成员变量、方法、构造器、初始化块、内部类包括接口、枚举5种成员而static可以修饰成员变量、方法、初始化块、内部类包括接口、枚举以static修饰的成员就是类成员。类成员属于整个类而不属于单个对象。
对static关键字而言有一条非常重要的规则类成员包括成员变量、方法、初始化块、内部类和内部枚举不能访问实例成员包括成员变量、方法、初始化块、内部类和内部枚举。因为类成员是属于类的类成员的作用域比实例成员的作用域更大完全可能出现类成员已经初始化完成但实例成员还不曾初始化的情况如果允许类成员访问实例成员将会引起大量错误。
1.42 static修饰的类能不能被继承
参考答案
static修饰的类可以被继承。
扩展阅读
如果使用static来修饰一个内部类则这个内部类就属于外部类本身而不属于外部类的某个对象。因此使用static修饰的内部类被称为类内部类有的地方也称为静态内部类。
static关键字的作用是把类的成员变成类相关而不是实例相关即static修饰的成员属于整个类而不属于单个对象。外部类的上一级程序单元是包所以不可使用static修饰而内部类的上一级程序单元是外部类使用static修饰可以将内部类变成外部类相关而不是外部类实例相关。因此static关键字不可修饰外部类但可修饰内部类。
静态内部类需满足如下规则 静态内部类可以包含静态成员也可以包含非静态成员 静态内部类不能访问外部类的实例成员只能访问它的静态成员 外部类的所有方法、初始化块都能访问其内部定义的静态内部类 在外部类的外部也可以实例化静态内部类语法如下 外部类.内部类 变量名 new 外部类.内部类构造方法();
1.43 static和final有什么区别
参考答案
static关键字可以修饰成员变量、成员方法、初始化块、内部类被static修饰的成员是类的成员它属于类、不属于单个对象。以下是static修饰这4种成员时表现出的特征
类变量被static修饰的成员变量叫类变量静态变量。类变量属于类它随类的信息存储在方法区并不随对象存储在堆中类变量可以通过类名来访问也可以通过对象名来访问但建议通过类名访问它。类方法被static修饰的成员方法叫类方法静态方法。类方法属于类可以通过类名访问也可以通过对象名访问建议通过类名访问它。静态块被static修饰的初始化块叫静态初始化块。静态块属于类它在类加载的时候被隐式调用一次之后便不会被调用了。静态内部类被static修饰的内部类叫静态内部类。静态内部类可以包含静态成员也可以包含非静态成员。静态内部类不能访问外部类的实例成员只能访问外部类的静态成员。外部类的所有方法、初始化块都能访问其内部定义的静态内部类。
final关键字可以修饰类、方法、变量以下是final修饰这3种目标时表现出的特征
final类final关键字修饰的类不可以被继承。final方法final关键字修饰的方法不可以被重写。final变量final关键字修饰的变量一旦获得了初始值就不可以被修改。
扩展阅读
变量分为成员变量、局部变量。
final修饰成员变量
类变量可以在声明变量时指定初始值也可以在静态初始化块中指定初始值实例变量可以在声明变量时指定初始值也可以在初始化块或构造方法中指定初始值
final修饰局部变量
可以在声明变量时指定初始值也可以在后面的代码中指定初始值。
注意被 final 修饰的任何形式的变量一旦获得了初始值就不可以被修改
泛型
1.44 说一说你对泛型的理解
参考答案
Java集合有个缺点—把一个对象“丢进”集合里之后集合就会“忘记”这个对象的数据类型当再次取出该对象时该对象的编译类型就变成了Object类型其运行时类型没变。
Java集合之所以被设计成这样是因为集合的设计者不知道我们会用集合来保存什么类型的对象所以他们把集合设计成能保存任何类型的对象只要求具有很好的通用性。但这样做带来如下两个问题
集合对元素类型没有任何限制这样可能引发一些问题。例如想创建一个只能保存Dog对象的集合但程序也可以轻易地将Cat对象“丢”进去所以可能引发异常。由于把对象“丢进”集合时集合丢失了对象的状态信息只知道它盛装的是Object因此取出集合元素后通常还需要进行强制类型转换。这种强制类型转换既增加了编程的复杂度也可能引发ClassCastException异常。
从Java 5开始Java引入了“参数化类型”的概念允许程序在创建集合时指定集合元素的类型Java的参数化类型被称为泛型Generic。例如 ListString表明该List只能保存字符串类型的对象。
有了泛型以后程序再也不能“不小心”地把其他对象“丢进”集合中。而且程序更加简洁集合自动记住所有集合元素的数据类型从而无须对集合元素进行强制类型转换。
1.45 介绍一下泛型擦除
参考答案
在严格的泛型代码里带泛型声明的类总应该带着类型参数。但为了与老的Java代码保持一致也允许在使用带泛型声明的类时不指定实际的类型。如果没有为这个泛型类指定实际的类型此时被称作raw type原始类型默认是声明该泛型形参时指定的第一个上限类型。
当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时所有在尖括号之间的类型信息都将被扔掉。比如一个 ListString 类型被转换为List则该List对集合元素的类型检查变成了泛型参数的上限即Object。
上述规则即为泛型擦除可以通过下面代码进一步理解泛型擦除
ListString list1 ...;List list2 list1; // list2将元素当做Object处理扩展阅读
从逻辑上来看ListString 是List的子类如果直接把一个List对象赋给一个ListString对象应该引起编译错误但实际上不会。对泛型而言可以直接把一个List对象赋给一个 ListString 对象编译器仅仅提示“未经检查的转换”。
上述规则叫做泛型转换可以通过下面代码进一步理解泛型转换
List list1 ...;ListString list2 list1; // 编译时警告“未经检查的转换”1.46 List? super T和List? extends T有什么区别
参考答案
? 是类型通配符List? 可以表示各种泛型List的父类意思是元素类型未知的ListList? super T 用于设定类型通配符的下限此处 ? 代表一个未知的类型但它必须是T的父类型List? extends T 用于设定类型通配符的上限此处 ? 代表一个未知的类型但它必须是T的子类型。
扩展阅读
在Java的早期设计中允许把Integer[]数组赋值给Number[]变量此时如果试图把一个Double对象保存到该Number[]数组中编译可以通过但在运行时抛出ArrayStoreException异常。这显然是一种不安全的设计因此Java在泛型设计时进行了改进它不再允许把 ListInteger 对象赋值给 ListNumber 变量。
数组和泛型有所不同假设Foo是Bar的一个子类型子类或者子接口那么Foo[]依然是Bar[]的子类型但GFoo 不是 GBar 的子类型。Foo[]自动向上转型为Bar[]的方式被称为型变也就是说Java的数组支持型变但Java集合并不支持型变。Java泛型的设计原则是只要代码在编译时没有出现警告就不会遇到运行时ClassCastException异常。
反射
1.47 说一说你对Java反射机制的理解
参考答案
Java程序中的对象在运行时可以表现为两种类型即编译时类型和运行时类型。例如 Person p new Student(); 这行代码将会生成一个p变量该变量的编译时类型为Person运行时类型为Student。
有时程序在运行时接收到外部传入的一个对象该对象的编译时类型是Object但程序又需要调用该对象的运行时类型的方法。这就要求程序需要在运行时发现对象和类的真实信息而解决这个问题有以下两种做法
第一种做法是假设在编译时和运行时都完全知道类型的具体信息在这种情况下可以先使用instanceof运算符进行判断再利用强制类型转换将其转换成其运行时类型的变量即可。第二种做法是编译时根本无法预知该对象和类可能属于哪些类程序只依靠运行时信息来发现该对象和类的真实信息这就必须使用反射。
具体来说通过反射机制我们可以实现如下的操作
程序运行时可以通过反射获得任意一个类的Class对象并通过这个对象查看这个类的信息程序运行时可以通过反射创建任意一个类的实例并访问该实例的成员程序运行时可以通过反射机制生成一个类的动态代理类或动态代理对象。
1.48 Java反射在实际项目中有哪些应用场景
参考答案
Java的反射机制在实际项目中应用广泛常见的应用场景有
使用JDBC时如果要创建数据库的连接则需要先通过反射机制加载数据库的驱动程序多数框架都支持注解/XML配置从配置中解析出来的类是字符串需要利用反射机制实例化面向切面编程AOP的实现方案是在程序运行时创建目标对象的代理类这必须由反射机制来实现。
1.49 说一说Java的四种引用方式
参考答案
Java对象的四种引用方式分别是强引用、软引用、弱引用、虚引用具体含义如下
强引用这是Java程序中最常见的引用方式即程序创建一个对象并把这个对象赋给一个引用变量程序通过该引用变量来操作实际的对象。当一个对象被一个或一个以上的引用变量所引用时它处于可达状态不可能被系统垃圾回收机制回收。软引用当一个对象只有软引用时它有可能被垃圾回收机制回收。对于只有软引用的对象而言当系统内存空间足够时它不会被系统回收程序也可使用该对象。当系统内存空间不足时系统可能会回收它。软引用通常用于对内存敏感的程序中。弱引用弱引用和软引用很像但弱引用的引用级别更低。对于只有弱引用的对象而言当系统垃圾回收机制运行时不管系统内存是否足够总会回收该对象所占用的内存。当然并不是说当一个对象只有弱引用时它就会立即被回收正如那些失去引用的对象一样必须等到系统垃圾回收机制运行时才会被回收。虚引用虚引用完全类似于没有引用。虚引用对对象本身没有太大影响对象甚至感觉不到虚引用的存在。如果一个对象只有一个虚引用时那么它和没有引用的效果大致相同。虚引用主要用于跟踪对象被垃圾回收的状态虚引用不能单独使用虚引用必须和引用队列联合使用。
2. 集合类
2.1 Java中有哪些容器集合类
参考答案
Java中的集合类主要由Collection和Map这两个接口派生而出其中Collection接口又派生出三个子接口分别是Set、List、Queue。所有的Java集合类都是Set、List、Queue、Map这四个接口的实现类这四个接口将集合分成了四大类其中
Set代表无序的元素不可重复的集合List代表有序的元素可以重复的集合Queue代表先进先出FIFO的队列Map代表具有映射关系key-value的集合。
这些接口拥有众多的实现类其中最常用的实现类有HashSet、TreeSet、ArrayList、LinkedList、ArrayDeque、HashMap、TreeMap等。
扩展阅读
Collection体系的继承树
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sGP2fKP3-1641469701679)(https://gitee.com/RedemptionXU/pic-md/raw/master/20220106193852.jpeg)]
Map体系的继承树 注紫色框体代表接口其中加粗的是代表四类集合的接口。蓝色框体代表实现类其中有阴影的是常用实现类。
2.2 Java中的容器线程安全和线程不安全的分别有哪些
参考答案
java.util包下的集合类大部分都是线程不安全的例如我们常用的HashSet、TreeSet、ArrayList、LinkedList、ArrayDeque、HashMap、TreeMap这些都是线程不安全的集合类但是它们的优点是性能好。如果需要使用线程安全的集合类则可以使用Collections工具类提供的synchronizedXxx()方法将这些集合类包装成线程安全的集合类。
java.util包下也有线程安全的集合类例如Vector、Hashtable。这些集合类都是比较古老的API虽然实现了线程安全但是性能很差。所以即便是需要使用线程安全的集合类也建议将线程不安全的集合类包装成线程安全集合类的方式而不是直接使用这些古老的API。
从Java5开始Java在java.util.concurrent包下提供了大量支持高效并发访问的集合类它们既能包装良好的访问性能有能包装线程安全。这些集合类可以分为两部分它们的特征如下 以Concurrent开头的集合类 以Concurrent开头的集合类代表了支持并发访问的集合它们可以支持多个线程并发写入访问这些写入线程的所有操作都是线程安全的但读取操作不必锁定。以Concurrent开头的集合类采用了更复杂的算法来保证永远不会锁住整个集合因此在并发写入时有较好的性能。 以CopyOnWrite开头的集合类 以CopyOnWrite开头的集合类采用复制底层数组的方式来实现写操作。当线程对此类集合执行读取操作时线程将会直接读取集合本身无须加锁与阻塞。当线程对此类集合执行写入操作时集合会在底层复制一份新的数组接下来对新的数组执行写入操作。由于对集合的写入操作都是对数组的副本执行操作因此它是线程安全的。
扩展阅读
java.util.concurrent包下线程安全的集合类的体系结构 2.3 Map接口有哪些实现类
参考答案
Map接口有很多实现类其中比较常用的有HashMap、LinkedHashMap、TreeMap、ConcurrentHashMap。
对于不需要排序的场景优先考虑使用HashMap因为它是性能最好的Map实现。如果需要保证线程安全则可以使用ConcurrentHashMap。它的性能好于Hashtable因为它在put时采用分段锁/CAS的加锁机制而不是像Hashtable那样无论是put还是get都做同步处理。
对于需要排序的场景如果需要按插入顺序排序则可以使用LinkedHashMap如果需要将key按自然顺序排列甚至是自定义顺序排列则可以选择TreeMap。如果需要保证线程安全则可以使用Collections工具类将上述实现类包装成线程安全的Map。
2.4 描述一下Map put的过程
参考答案
HashMap是最经典的Map实现下面以它的视角介绍put的过程 首次扩容 先判断数组是否为空若数组为空则进行第一次扩容resize 计算索引 通过hash算法计算键值对在数组中的索引 插入数据 如果当前位置元素为空则直接插入数据如果当前位置元素非空且key已存在则直接覆盖其value如果当前位置元素非空且key不存在则将数据链到链表末端若链表长度达到8则将链表转换成红黑树并将数据插入树中 再次扩容 如果数组中元素个数size超过threshold则再次进行扩容操作。
扩展阅读
HashMap添加数据的详细过程如下图 2.5 如何得到一个线程安全的Map
参考答案
使用Collections工具类将线程不安全的Map包装成线程安全的Map使用java.util.concurrent包下的Map如ConcurrentHashMap不建议使用Hashtable虽然Hashtable是线程安全的但是性能较差。
2.6 HashMap有什么特点
参考答案
HashMap是线程不安全的实现HashMap可以使用null作为key或value。
2.7 JDK7和JDK8中的HashMap有什么区别
参考答案
JDK7中的HashMap是基于数组链表来实现的它的底层维护一个Entry数组。它会根据计算的hashCode将对应的KV键值对存储到该数组中一旦发生hashCode冲突那么就会将该KV键值对放到对应的已有元素的后面 此时便形成了一个链表式的存储结构。
JDK7中HashMap的实现方案有一个明显的缺点即当Hash冲突严重时在桶上形成的链表会变得越来越长这样在查询时的效率就会越来越低其时间复杂度为O(N)。
JDK8中的HashMap是基于数组链表红黑树来实现的它的底层维护一个Node数组。当链表的存储的数据个数大于等于8的时候不再采用链表存储而采用了红黑树存储结构。这么做主要是在查询的时间复杂度上进行优化链表为O(N)而红黑树一直是O(logN)可以大大的提高查找性能。
2.8 介绍一下HashMap底层的实现原理
参考答案
它基于hash算法通过put方法和get方法存储和获取对象。
存储对象时我们将K/V传给put方法时它调用K的hashCode计算hash从而得到bucket位置进一步存储HashMap会根据当前bucket的占用情况自动调整容量(超过Load Facotr则resize为原来的2倍)。获取对象时我们将K传给get它调用hashCode计算hash从而得到bucket位置并进一步调用equals()方法确定键值对。
如果发生碰撞的时候HashMap通过链表将产生碰撞冲突的元素组织起来。在Java 8中如果一个bucket中碰撞冲突的元素超过某个限制(默认是8)则使用红黑树来替换链表从而提高速度。
2.9 介绍一下HashMap的扩容机制
参考答案
数组的初始容量为16而容量是以2的次方扩充的一是为了提高性能使用足够大的数组二是为了能使用位运算代替取模预算(据说提升了5~8倍)。数组是否需要扩充是通过负载因子判断的如果当前元素个数为数组容量的0.75时就会扩充数组。这个0.75就是默认的负载因子可由构造器传入。我们也可以设置大于1的负载因子这样数组就不会扩充牺牲性能节省内存。为了解决碰撞数组中的元素是单向链表类型。当链表长度到达一个阈值时7或8会将链表转换成红黑树提高性能。而当链表长度缩小到另一个阈值时6又会将红黑树转换回单向链表提高性能。对于第三点补充说明检查链表长度转换成红黑树之前还会先检测当前数组数组是否到达一个阈值64如果没有到达这个容量会放弃转换先去扩充数组。所以上面也说了链表长度的阈值是7或8因为会有一次放弃转换的操作。
扩展阅读
例如我们从16扩展为32时具体的变化如下所示 因此元素在重新计算hash之后因为n变为2倍那么n-1的mask范围在高位多1bit(红色)因此新的index就会发生这样的变化 因此我们在扩充HashMap的时候不需要重新计算hash只需要看看原来的hash值新增的那个bit是1还是0就好了是0的话索引没变是1的话索引变成“原索引oldCap”。可以看看下图为16扩充为32的resize示意图 这个设计确实非常的巧妙既省去了重新计算hash值的时间而且同时由于新增的1bit是0还是1可以认为是随机的因此resize的过程均匀的把之前的冲突的节点分散到新的bucket了。
2.10 HashMap中的循环链表是如何产生的
参考答案
在多线程的情况下当重新调整HashMap大小的时候就会存在条件竞争因为如果两个线程都发现HashMap需要重新调整大小了它们会同时试着调整大小。在调整大小的过程中存储在链表中的元素的次序会反过来因为移动到新的bucket位置的时候HashMap并不会将元素放在链表的尾部而是放在头部这是为了避免尾部遍历。如果条件竞争发生了那么就会产生死循环了。
2.11 HashMap为什么用红黑树而不用B树
参考答案
B/B树多用于外存上时B/B也被成为一个磁盘友好的数据结构。
HashMap本来是数组链表的形式链表由于其查找慢的特点所以需要被查找效率更高的树结构来替换。如果用B/B树的话在数据量不是很多的情况下数据都会“挤在”一个结点里面这个时候遍历效率就退化成了链表。
2.12 HashMap为什么线程不安全
参考答案
HashMap在并发执行put操作时可能会导致形成循环链表从而引起死循环。
2.13 HashMap如何实现线程安全
参考答案
直接使用Hashtable类直接使用ConcurrentHashMap使用Collections将HashMap包装成线程安全的Map。
2.14 HashMap是如何解决哈希冲突的
参考答案
为了解决碰撞数组中的元素是单向链表类型。当链表长度到达一个阈值时会将链表转换成红黑树提高性能。而当链表长度缩小到另一个阈值时又会将红黑树转换回单向链表提高性能。
2.15 说一说HashMap和HashTable的区别
参考答案
Hashtable是一个线程安全的Map实现但HashMap是线程不安全的实现所以HashMap比Hashtable的性能高一点。Hashtable不允许使用null作为key和value如果试图把null值放进Hashtable中将会引发空指针异常但HashMap可以使用null作为key或value。
扩展阅读
从Hashtable的类名上就可以看出它是一个古老的类它的命名甚至没有遵守Java的命名规范每个单词的首字母都应该大写。也许当初开发Hashtable的工程师也没有注意到这一点后来大量Java程序中使用了Hashtable类所以这个类名也就不能改为HashTable了否则将导致大量程序需要改写。
与Vector类似的是尽量少用Hashtable实现类即使需要创建线程安全的Map实现类也无须使用Hashtable实现类可以通过Collections工具类把HashMap变成线程安全的Map。
2.16 HashMap与ConcurrentHashMap有什么区别
参考答案
HashMap是非线程安全的这意味着不应该在多线程中对这些Map进行修改操作否则会产生数据不一致的问题甚至还会因为并发插入元素而导致链表成环这样在查找时就会发生死循环影响到整个应用程序。
Collections工具类可以将一个Map转换成线程安全的实现其实也就是通过一个包装类然后把所有功能都委托给传入的Map而包装类是基于synchronized关键字来保证线程安全的Hashtable也是基于synchronized关键字底层使用的是互斥锁性能与吞吐量比较低。
ConcurrentHashMap的实现细节远没有这么简单因此性能也要高上许多。它没有使用一个全局锁来锁住自己而是采用了减少锁粒度的方法尽量减少因为竞争锁而导致的阻塞与冲突而且ConcurrentHashMap的检索操作是不需要锁的。
2.17 介绍一下ConcurrentHashMap是怎么实现的
参考答案
JDK 1.7中的实现
在 jdk 1.7 中ConcurrentHashMap 是由 Segment 数据结构和 HashEntry 数组结构构成采取分段锁来保证安全性。Segment 是 ReentrantLock 重入锁在 ConcurrentHashMap 中扮演锁的角色HashEntry 则用于存储键值对数据。一个 ConcurrentHashMap 里包含一个 Segment 数组一个 Segment 里包含一个 HashEntry 数组Segment 的结构和 HashMap 类似是一个数组和链表结构。 JDK 1.8中的实现
JDK1.8 的实现已经摒弃了 Segment 的概念而是直接用 Node 数组链表红黑树的数据结构来实现并发控制使用 Synchronized 和 CAS 来操作整个看起来就像是优化过且线程安全的 HashMap虽然在 JDK1.8 中还能看到 Segment 的数据结构但是已经简化了属性只是为了兼容旧版本。 2.18 ConcurrentHashMap是怎么分段分组的
参考答案
get操作
Segment的get操作实现非常简单和高效先经过一次再散列然后使用这个散列值通过散列运算定位到 Segment再通过散列算法定位到元素。get操作的高效之处在于整个get过程都不需要加锁除非读到空的值才会加锁重读。原因就是将使用的共享变量定义成 volatile 类型。
put操作
当执行put操作时会经历两个步骤
判断是否需要扩容定位到添加元素的位置将其放入 HashEntry 数组中。
插入过程会进行第一次 key 的 hash 来定位 Segment 的位置如果该 Segment 还没有初始化即通过 CAS 操作进行赋值然后进行第二次 hash 操作找到相应的 HashEntry 的位置这里会利用继承过来的锁的特性在将数据插入指定的 HashEntry 位置时尾插法会通过继承 ReentrantLock 的 tryLock() 方法尝试去获取锁如果获取成功就直接插入相应的位置如果已经有线程获取该Segment的锁那当前线程会以自旋的方式去继续的调用 tryLock() 方法去获取锁超过指定次数就挂起等待唤醒。
2.19 说一说你对LinkedHashMap的理解
参考答案
LinkedHashMap使用双向链表来维护key-value对的顺序其实只需要考虑key的顺序该链表负责维护Map的迭代顺序迭代顺序与key-value对的插入顺序保持一致。
LinkedHashMap可以避免对HashMap、Hashtable里的key-value对进行排序只要插入key-value对时保持顺序即可同时又可避免使用TreeMap所增加的成本。
LinkedHashMap需要维护元素的插入顺序因此性能略低于HashMap的性能。但因为它以链表来维护内部顺序所以在迭代访问Map里的全部元素时将有较好的性能。
2.20 请介绍LinkedHashMap的底层原理
参考答案
LinkedHashMap继承于HashMap它在HashMap的基础上通过维护一条双向链表解决了HashMap不能随时保持遍历顺序和插入顺序一致的问题。在实现上LinkedHashMap很多方法直接继承自HashMap仅为维护双向链表重写了部分方法。
如下图淡蓝色的箭头表示前驱引用红色箭头表示后继引用。每当有新的键值对节点插入时新节点最终会接在tail引用指向的节点后面。而tail引用则会移动到新的节点上这样一个双向链表就建立起来了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tnTK8heo-1641469701690)(https://gitee.com/RedemptionXU/pic-md/raw/master/20220106194101.jpeg)]
2.21 请介绍TreeMap的底层原理
参考答案
TreeMap基于红黑树Red-Black tree实现。映射根据其键的自然顺序进行排序或者根据创建映射时提供的 Comparator 进行排序具体取决于使用的构造方法。TreeMap的基本操作containsKey、get、put、remove方法它的时间复杂度是log(N)。
TreeMap包含几个重要的成员变量root、size、comparator。其中root是红黑树的根节点。它是Entry类型Entry是红黑树的节点它包含了红黑树的6个基本组成key、value、left、right、parent和color。Entry节点根据根据Key排序包含的内容是value。Entry中key比较大小是根据比较器comparator来进行判断的。size是红黑树的节点个数。
2.22 Map和Set有什么区别
参考答案
Set代表无序的元素不可重复的集合
Map代表具有映射关系key-value的集合其所有的key是一个Set集合即key无序且不能重复。
2.23 List和Set有什么区别
参考答案
Set代表无序的元素不可重复的集合
List代表有序的元素可以重复的集合。
2.24 ArrayList和LinkedList有什么区别
参考答案
ArrayList的实现是基于数组LinkedList的实现是基于双向链表对于随机访问ArrayList要优于LinkedListArrayList可以根据下标以O(1)时间复杂度对元素进行随机访问而LinkedList的每一个元素都依靠地址指针和它后一个元素连接在一起查找某个元素的时间复杂度是O(N)对于插入和删除操作LinkedList要优于ArrayList因为当元素被添加到LinkedList任意位置的时候不需要像ArrayList那样重新计算大小或者是更新索引LinkedList比ArrayList更占内存因为LinkedList的节点除了存储数据还存储了两个引用一个指向前一个元素一个指向后一个元素。
2.25 有哪些线程安全的List
参考答案 Vector Vector是比较古老的API虽然保证了线程安全但是由于效率低一般不建议使用。 Collections.SynchronizedList SynchronizedList是Collections的内部类Collections提供了synchronizedList方法可以将一个线程不安全的List包装成线程安全的List即SynchronizedList。它比Vector有更好的扩展性和兼容性但是它所有的方法都带有同步锁也不是性能最优的List。 CopyOnWriteArrayList CopyOnWriteArrayList是Java 1.5在java.util.concurrent包下增加的类它采用复制底层数组的方式来实现写操作。当线程对此类集合执行读取操作时线程将会直接读取集合本身无须加锁与阻塞。当线程对此类集合执行写入操作时集合会在底层复制一份新的数组接下来对新的数组执行写入操作。由于对集合的写入操作都是对数组的副本执行操作因此它是线程安全的。在所有线程安全的List中它是性能最优的方案。
2.26 介绍一下ArrayList的数据结构
参考答案
ArrayList的底层是用数组来实现的默认第一次插入元素时创建大小为10的数组超出限制时会增加50%的容量并且数据以 System.arraycopy() 复制到新的数组因此最好能给出数组大小的预估值。
按数组下标访问元素的性能很高这是数组的基本优势。直接在数组末尾加入元素的性能也高但如果按下标插入、删除元素则要用 System.arraycopy() 来移动部分受影响的元素性能就变差了这是基本劣势。
2.27 谈谈CopyOnWriteArrayList的原理
参考答案
CopyOnWriteArrayList是Java并发包里提供的并发类简单来说它就是一个线程安全且读操作无锁的ArrayList。正如其名字一样在写操作时会复制一份新的List在新的List上完成写操作然后再将原引用指向新的List。这样就保证了写操作的线程安全。
CopyOnWriteArrayList允许线程并发访问读操作这个时候是没有加锁限制的性能较高。而写操作的时候则首先将容器复制一份然后在新的副本上执行写操作这个时候写操作是上锁的。结束之后再将原容器的引用指向新容器。注意在上锁执行写操作的过程中如果有需要读操作会作用在原容器上。因此上锁的写操作不会影响到并发访问的读操作。
优点读操作性能很高因为无需任何同步措施比较适用于读多写少的并发场景。在遍历传统的List时若中途有别的线程对其进行修改则会抛出ConcurrentModificationException异常。而CopyOnWriteArrayList由于其读写分离的思想遍历和修改操作分别作用在不同的List容器所以在使用迭代器进行遍历时候也就不会抛出ConcurrentModificationException异常了。缺点一是内存占用问题毕竟每次执行写操作都要将原容器拷贝一份数据量大时对内存压力较大可能会引起频繁GC。二是无法保证实时性Vector对于读写操作均加锁同步可以保证读和写的强一致性。而CopyOnWriteArrayList由于其实现策略的原因写和读分别作用在新老不同容器上在写操作执行过程中读不会阻塞但读取到的却是老容器的数据。
2.28 说一说TreeSet和HashSet的区别
参考答案
HashSet、TreeSet中的元素都是不能重复的并且它们都是线程不安全的二者的区别是
HashSet中的元素可以是null但TreeSet中的元素不能是nullHashSet不能保证元素的排列顺序而TreeSet支持自然排序、定制排序两种排序的方式HashSet底层是采用哈希表实现的而TreeSet底层是采用红黑树实现的。
2.29 说一说HashSet的底层结构
参考答案
HashSet是基于HashMap实现的默认构造函数是构建一个初始容量为16负载因子为0.75 的HashMap。它封装了一个 HashMap 对象来存储所有的集合元素所有放入 HashSet 中的集合元素实际上由 HashMap 的 key 来保存而 HashMap 的 value 则存储了一个 PRESENT它是一个静态的 Object 对象。
2.30 BlockingQueue中有哪些方法为什么这样设计
参考答案
为了应对不同的业务场景BlockingQueue 提供了4 组不同的方法用于插入、移除以及对队列中的元素进行检查。如果请求的操作不能得到立即执行的话每组方法的表现是不同的。这些方法如下 四组不同的行为方式含义如下
抛异常如果操作无法立即执行则抛一个异常特定值如果操作无法立即执行则返回一个特定的值(一般是 true / false)。阻塞如果操作无法立即执行则该方法调用将会发生阻塞直到能够执行超时如果操作无法立即执行则该方法调用将会发生阻塞直到能够执行。但等待时间不会超过给定值并返回一个特定值以告知该操作是否成功(典型的是true / false)。
2.31 BlockingQueue是怎么实现的
参考答案
BlockingQueue是一个接口它的实现类有ArrayBlockingQueue、DelayQueue、 LinkedBlockingQueue、PriorityBlockingQueue、SynchronousQueue等。它们的区别主要体现在存储结构上或对元素操作上的不同但是对于put与take操作的原理是类似的。下面以ArrayBlockingQueue为例来说明BlockingQueue的实现原理。
首先看一下ArrayBlockingQueue的构造函数它初始化了put和take函数中用到的关键成员变量这两个变量的类型分别是ReentrantLock和Condition。ReentrantLock是AbstractQueuedSynchronizerAQS的子类它的newCondition函数返回的Condition实例是定义在AQS类内部的ConditionObject类该类可以直接调用AQS相关的函数。
public ArrayBlockingQueue(int capacity, boolean fair) {if (capacity 0)throw new IllegalArgumentException();this.items new Object[capacity];lock new ReentrantLock(fair);notEmpty lock.newCondition();notFull lock.newCondition();
}put函数会在队列末尾添加元素如果队列已经满了无法添加元素的话就一直阻塞等待到可以加入为止。函数的源码如下所示。我们会发现put函数使用了wait/notify的机制。与一般生产者-消费者的实现方式不同同步队列使用ReentrantLock和Condition相结合的机制即先获得锁再等待而不是synchronized和wait的机制。
public void put(E e) throws InterruptedException {checkNotNull(e);final ReentrantLock lock this.lock;lock.lockInterruptibly();try {while (count items.length) notFull.await();enqueue(e);} finally {lock.unlock();}
}再来看一下消费者调用的take函数take函数在队列为空时会被阻塞一直到阻塞队列加入了新的元素。
public E take() throws InterruptedException {final ReentrantLock lock this.lock;lock.lockInterruptibly();try {while (count 0)notEmpty.await();return dequeue();} finally {lock.unlock();}
}扩展阅读
await操作
我们发现ArrayBlockingQueue并没有使用Object.wait而是使用的Condition.await这是为什么呢Condition对象可以提供和Object的wait和notify一样的行为但是后者必须先获取synchronized这个内置的monitor锁才能调用而Condition则必须先获取ReentrantLock。这两种方式在阻塞等待时都会将相应的锁释放掉但是Condition的等待可以中断这是二者唯一的区别。
我们先来看一下Condition的await函数await函数的流程大致如下图所示。await函数主要有三个步骤一是调用addConditionWaiter函数在condition wait queue队列中添加一个节点代表当前线程在等待一个消息。然后调用fullyRelease函数将持有的锁释放掉调用的是AQS的函数。最后一直调用isOnSyncQueue函数判断节点是否被转移到sync queue队列上也就是AQS中等待获取锁的队列。如果没有则进入阻塞状态如果已经在队列上则调用acquireQueued函数重新获取锁。 signal操作
signal函数将condition wait queue队列中队首的线程节点转移等待获取锁的sync queue队列中。这样的话await函数中调用isOnSyncQueue函数就会返回true导致await函数进入最后一步重新获取锁的状态。
我们这里来详细解析一下condition wait queue和sync queue两个队列的设计原理。condition wait queue是等待消息的队列因为阻塞队列为空而进入阻塞状态的take函数操作就是在等待阻塞队列不为空的消息。而sync queue队列则是等待获取锁的队列take函数获得了消息就可以运行了但是它还必须等待获取锁之后才能真正进行运行状态。
signal函数其实就做了一件事情就是不断尝试调用transferForSignal函数将condition wait queue队首的一个节点转移到sync queue队列中直到转移成功。因为一次转移成功就代表这个消息被成功通知到了等待消息的节点。
signal函数的示意图如下所示。 2.32 Stream不是IOStream有哪些方法
参考答案
Stream提供了大量的方法进行聚集操作这些方法既可以是“中间的”也可以是“末端的”。
中间方法中间操作允许流保持打开状态并允许直接调用后续方法。上面程序中的map()方法就是中间方法。中间方法的返回值是另外一个流。末端方法末端方法是对流的最终操作。当对某个Stream执行末端方法后该流将会被“消耗”且不再可用。上面程序中的sum()、count()、average()等方法都是末端方法。
除此之外关于流的方法还有如下两个特征
有状态的方法这种方法会给流增加一些新的属性比如元素的唯一性、元素的最大数量、保证元素以排序的方式被处理等。有状态的方法往往需要更大的性能开销。短路方法短路方法可以尽早结束对流的操作不必检查所有的元素。
下面简单介绍一下Stream常用的中间方法
filter(Predicate predicate)过滤Stream中所有不符合predicate的元素。mapToXxx(ToXxxFunction mapper)使用ToXxxFunction对流中的元素执行一对一的转换该方法返回的新流中包含了ToXxxFunction转换生成的所有元素。peek(Consumer action)依次对每个元素执行一些操作该方法返回的流与原有流包含相同的元素。该方法主要用于调试。distinct()该方法用于排序流中所有重复的元素判断元素重复的标准是使用equals()比较返回true。这是一个有状态的方法。sorted()该方法用于保证流中的元素在后续的访问中处于有序状态。这是一个有状态的方法。limit(long maxSize)该方法用于保证对该流的后续访问中最大允许访问的元素个数。这是一个有状态的、短路方法。
下面简单介绍一下Stream常用的末端方法
forEach(Consumer action)遍历流中所有元素对每个元素执行action。toArray()将流中所有元素转换为一个数组。reduce()该方法有三个重载的版本都用于通过某种操作来合并流中的元素。min()返回流中所有元素的最小值。max()返回流中所有元素的最大值。count()返回流中所有元素的数量。anyMatch(Predicate predicate)判断流中是否至少包含一个元素符合Predicate条件。noneMatch(Predicate predicate)判断流中是否所有元素都不符合Predicate条件。findFirst()返回流中的第一个元素。findAny()返回流中的任意一个元素。
除此之外Java 8允许使用流式API来操作集合Collection接口提供了一个stream()默认方法该方法可返回该集合对应的流接下来即可通过流式API来操作集合元素。由于Stream可以对集合元素进行整体的聚集操作因此Stream极大地丰富了集合的功能。
扩展阅读
Java 8新增了Stream、IntStream、LongStream、DoubleStream等流式API这些API代表多个支持串行和并行聚集操作的元素。上面4个接口中Stream是一个通用的流接口而IntStream、LongStream、DoubleStream则代表元素类型为int、long、double的流。
Java 8还为上面每个流式API提供了对应的Builder例如Stream.Builder、IntStream.Builder、LongStream.Builder、DoubleStream.Builder开发者可以通过这些Builder来创建对应的流。
独立使用Stream的步骤如下
使用Stream或XxxStream的builder()类方法创建该Stream对应的Builder。重复调用Builder的add()方法向该流中添加多个元素。调用Builder的build()方法获取对应的Stream。调用Stream的聚集方法。
在上面4个步骤中第4步可以根据具体需求来调用不同的方法Stream提供了大量的聚集方法供用户调用具体可参考Stream或XxxStream的API文档。对于大部分聚集方法而言每个Stream只能执行一次。
3. IO
3.1 介绍一下Java中的IO流
参考答案
IOInput Output用于实现对数据的输入与输出操作Java把不同的输入/输出源键盘、文件、网络等抽象表述为流Stream。流是从起源到接收的有序数据有了它程序就可以采用同一方式访问不同的输入/输出源。
按照数据流向可以将流分为输入流和输出流其中输入流只能读取数据、不能写入数据而输出流只能写入数据、不能读取数据。按照数据类型可以将流分为字节流和字符流其中字节流操作的数据单元是8位的字节而字符流操作的数据单元是16位的字符。按照处理功能可以将流分为节点流和处理流其中节点流可以直接从/向一个特定的IO设备磁盘、网络等读/写数据也称为低级流而处理流是对节点流的连接或封装用于简化数据读/写功能或提高效率也称为高级流。
Java提供了大量的类来支持IO操作下表给大家整理了其中比较常用的一些类。其中黑色字体的是抽象基类其他所有的类都继承自它们。红色字体的是节点流蓝色字体的是处理流。 根据命名很容易理解各个流的作用
以File开头的文件流用于访问文件以ByteArray/CharArray开头的流用于访问内存中的数组以Piped开头的管道流用于访问管道实现进程之间的通信以String开头的流用于访问内存中的字符串以Buffered开头的缓冲流用于在读写数据时对数据进行缓存以减少IO次数InputStreamReader、InputStreamWriter是转换流用于将字节流转换为字符流以Object开头的流是对象流用于实现对象的序列化以Print开头的流是打印流用于简化打印操作以Pushback开头的流是推回输入流用于将已读入的数据推回到缓冲区从而实现再次读取以Data开头的流是特殊流用于读写Java基本类型的数据。
3.2 怎么用流打开一个大文件
参考答案
打开大文件应避免直接将文件中的数据全部读取到内存中可以采用分次读取的方式。
使用缓冲流。缓冲流内部维护了一个缓冲区通过与缓冲区的交互减少与设备的交互次数。使用缓冲输入流时它每次会读取一批数据将缓冲区填满每次调用读取方法并不是直接从设备取值而是从缓冲区取值当缓冲区为空时它会再一次读取数据将缓冲区填满。使用缓冲输出流时每次调用写入方法并不是直接写入到设备而是写入缓冲区当缓冲区填满时它会自动刷入设备。使用NIO。NIO采用内存映射文件的方式来处理输入/输出NIO将文件或文件的一段区域映射到内存中这样就可以像访问内存一样来访问文件了这种方式模拟了操作系统上的虚拟内存的概念通过这种方式来进行输入/输出比传统的输入/输出要快得多。
3.4 说说NIO的实现原理
参考答案
Java的NIO主要由三个核心部分组成Channel、Buffer、Selector。
基本上所有的IO在NIO中都从一个Channel开始数据可以从Channel读到Buffer中也可以从Buffer写到Channel中。Channel有好几种类型其中比较常用的有FileChannel、DatagramChannel、SocketChannel、ServerSocketChannel等这些通道涵盖了UDP和TCP网络IO以及文件IO。
Buffer本质上是一块可以写入数据然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象并提供了一组方法用来方便的访问该块内存。Java NIO里关键的Buffer实现有CharBuffer、ByteBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。这些Buffer覆盖了你能通过IO发送的基本数据类型即byte、short、int、long、float、double、char。
Buffer对象包含三个重要的属性分别是capacity、position、limit其中position和limit的含义取决于Buffer处在读模式还是写模式。但不管Buffer处在什么模式capacity的含义总是一样的。
capacity作为一个内存块Buffer有个固定的最大值就是capacity。Buffer只能写capacity个数据一旦Buffer满了需要将其清空才能继续写数据往里写数据。position当写数据到Buffer中时position表示当前的位置。初始的position值为0。当一个数据写到Buffer后 position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity–1。当读取数据时也是从某个特定位置读。当将Buffer从写模式切换到读模式position会被重置为0。当从Buffer的position处读取数据时position向前移动到下一个可读的位置。limit在写模式下Buffer的limit表示最多能往Buffer里写多少数据此时limit等于capacity。当切换Buffer到读模式时 limit表示你最多能读到多少数据此时limit会被设置成写模式下的position值。
三个属性之间的关系如下图所示 Selector允许单线程处理多个 Channel如果你的应用打开了多个连接通道但每个连接的流量都很低使用Selector就会很方便。要使用Selector得向Selector注册Channel然后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回线程就可以处理这些事件事件例如有新连接进来数据接收等。
这是在一个单线程中使用一个Selector处理3个Channel的图示 扩展阅读
Java NIO根据操作系统不同 针对NIO中的Selector有不同的实现
macosxKQueueSelectorProvidersolarisDevPollSelectorProviderLinuxEPollSelectorProvider (Linux kernels 2.6)或PollSelectorProviderwindowsWindowsSelectorProvider
所以不需要特别指定Oracle JDK会自动选择合适的Selector。如果想设置特定的Selector可以设置属性例如 -Djava.nio.channels.spi.SelectorProvidersun.nio.ch.EPollSelectorProvider。
JDK在Linux已经默认使用epoll方式但是JDK的epoll采用的是水平触发所以Netty自4.0.16起, Netty为Linux通过JNI的方式提供了native socket transport。Netty重新实现了epoll机制。
采用边缘触发方式netty epoll transport暴露了更多的nio没有的配置参数如 TCP_CORK, SO_REUSEADDR等等C代码更少GC更少synchronized。
3.5 介绍一下Java的序列化与反序列化
参考答案
序列化机制可以将对象转换成字节序列这些字节序列可以保存在磁盘上也可以在网络中传输并允许程序将这些字节序列再次恢复成原来的对象。其中对象的序列化Serialize是指将一个Java对象写入IO流中对象的反序列化Deserialize则是指从IO流中恢复该Java对象。
若对象要支持序列化机制则它的类需要实现Serializable接口该接口是一个标记接口它没有提供任何方法只是标明该类是可以序列化的Java的很多类已经实现了Serializable接口如包装类、String、Date等。
若要实现序列化则需要使用对象流ObjectInputStream和ObjectOutputStream。其中在序列化时需要调用ObjectOutputStream对象的writeObject()方法以输出对象序列。在反序列化时需要调用ObjectInputStream对象的readObject()方法将对象序列恢复为对象。
3.6 Serializable接口为什么需要定义serialVersionUID变量
参考答案
serialVersionUID代表序列化的版本通过定义类的序列化版本在反序列化时只要对象中所存的版本和当前类的版本一致就允许做恢复数据的操作否则将会抛出序列化版本不一致的错误。
如果不定义序列化版本在反序列化时可能出现冲突的情况例如
创建该类的实例并将这个实例序列化保存在磁盘上升级这个类例如增加、删除、修改这个类的成员变量反序列化该类的实例即从磁盘上恢复修改之前保存的数据。
在第3步恢复数据的时候当前的类已经和序列化的数据的格式产生了冲突可能会发生各种意想不到的问题。增加了序列化版本之后在这种情况下则可以抛出异常以提示这种矛盾的存在提高数据的安全性。
3.7 除了Java自带的序列化之外你还了解哪些序列化工具
参考答案
JSON目前使用比较频繁的格式化数据工具简单直观可读性好有jacksongsonfastjson等等比较优秀的JSON解析工具的表现还是比较好的有些json解析工具甚至速度超过了一些二进制的序列化方式。Protobuf一个用来序列化结构化数据的技术支持多种语言诸如C、Java以及Python语言可以使用该技术来持久化数据或者序列化成网络传输的数据。相比较一些其他的XML技术而言该技术的一个明显特点就是更加节省空间以二进制流存储、速度更快以及更加灵活。另外Protobuf支持的数据类型相对较少不支持常量类型。由于其设计的理念是纯粹的展现层协议Presentation Layer目前并没有一个专门支持Protobuf的RPC框架。Thrift是Facebook开源提供的一个高性能轻量级RPC服务框架其产生正是为了满足当前大数据量、分布式、跨语言、跨平台数据通讯的需求。 但是Thrift并不仅仅是序列化协议而是一个RPC框架。 相对于JSON和XML而言Thrift在空间开销和解析性能上有了比较大的提升对于对性能要求比较高的分布式系统它是一个优秀的RPC解决方案。但是由于Thrift的序列化被嵌入到Thrift框架里面 Thrift框架本身并没有透出序列化和反序列化接口这导致其很难和其他传输层协议共同使用例如HTTP。Avro提供两种序列化格式即JSON格式或者Binary格式。Binary格式在空间开销和解析性能方面可以和Protobuf媲美 JSON格式方便测试阶段的调试。 Avro支持的数据类型非常丰富包括C语言里面的union类型。Avro支持JSON格式的IDL和类似于Thrift和Protobuf的IDL实验阶段这两者之间可以互转。Schema可以在传输数据的同时发送加上JSON的自我描述属性这使得Avro非常适合动态类型语言。 Avro在做文件持久化的时候一般会和Schema一起存储所以Avro序列化文件自身具有自我描述属性所以非常适合于做Hive、Pig和MapReduce的持久化数据格式。对于不同版本的Schema在进行RPC调用的时候服务端和客户端可以在握手阶段对Schema进行互相确认大大提高了最终的数据解析速度。
3.8 如果不用JSON工具该如何实现对实体类的序列化
参考答案
可以使用Java原生的序列化机制但是效率比较低一些适合小项目
创建该类的实例并将这个实例序列化保存在磁盘上升级这个类例如增加、删除、修改这个类的成员变量反序列化该类的实例即从磁盘上恢复修改之前保存的数据。
在第3步恢复数据的时候当前的类已经和序列化的数据的格式产生了冲突可能会发生各种意想不到的问题。增加了序列化版本之后在这种情况下则可以抛出异常以提示这种矛盾的存在提高数据的安全性。
3.7 除了Java自带的序列化之外你还了解哪些序列化工具
参考答案
JSON目前使用比较频繁的格式化数据工具简单直观可读性好有jacksongsonfastjson等等比较优秀的JSON解析工具的表现还是比较好的有些json解析工具甚至速度超过了一些二进制的序列化方式。Protobuf一个用来序列化结构化数据的技术支持多种语言诸如C、Java以及Python语言可以使用该技术来持久化数据或者序列化成网络传输的数据。相比较一些其他的XML技术而言该技术的一个明显特点就是更加节省空间以二进制流存储、速度更快以及更加灵活。另外Protobuf支持的数据类型相对较少不支持常量类型。由于其设计的理念是纯粹的展现层协议Presentation Layer目前并没有一个专门支持Protobuf的RPC框架。Thrift是Facebook开源提供的一个高性能轻量级RPC服务框架其产生正是为了满足当前大数据量、分布式、跨语言、跨平台数据通讯的需求。 但是Thrift并不仅仅是序列化协议而是一个RPC框架。 相对于JSON和XML而言Thrift在空间开销和解析性能上有了比较大的提升对于对性能要求比较高的分布式系统它是一个优秀的RPC解决方案。但是由于Thrift的序列化被嵌入到Thrift框架里面 Thrift框架本身并没有透出序列化和反序列化接口这导致其很难和其他传输层协议共同使用例如HTTP。Avro提供两种序列化格式即JSON格式或者Binary格式。Binary格式在空间开销和解析性能方面可以和Protobuf媲美 JSON格式方便测试阶段的调试。 Avro支持的数据类型非常丰富包括C语言里面的union类型。Avro支持JSON格式的IDL和类似于Thrift和Protobuf的IDL实验阶段这两者之间可以互转。Schema可以在传输数据的同时发送加上JSON的自我描述属性这使得Avro非常适合动态类型语言。 Avro在做文件持久化的时候一般会和Schema一起存储所以Avro序列化文件自身具有自我描述属性所以非常适合于做Hive、Pig和MapReduce的持久化数据格式。对于不同版本的Schema在进行RPC调用的时候服务端和客户端可以在握手阶段对Schema进行互相确认大大提高了最终的数据解析速度。
3.8 如果不用JSON工具该如何实现对实体类的序列化
参考答案
可以使用Java原生的序列化机制但是效率比较低一些适合小项目
可以使用其他的一些第三方类库比如Protobuf、Thrift、Avro等。