1.什么是面向对象,谈谈你对面向对象的理解?
-
面向对象是一种思想,简单来说就是将数据和操作数据的方法封装在对象中。举个例子来说比如洗衣机洗衣服。我们通常会把这个拆分成两个对象----人和洗衣机。人需要干的就是:打开洗衣机---放入衣服---放入洗衣液---关闭洗衣机门-按下各种开关,洗衣机则负责:清洗---烘干
-
面向对象拥有三大特性其实也可以说四大特性:封装--继承--多态--抽象
-
相等于封装来说,就是把一切内部信息隐藏起来,对外不透明,只提供最简单的调用。
-
继承是从已有的一个类得到信息并创建新类的过程,一般我们称这个提供信息的类为父类,得到信息的的类为子类。子类可以扩展自己的信息,按我自己的理解,继承就是一种信息复用,也是信息延申的一个手段。
-
多态存在的必要三个条件就是继承,方法的重写,父类引用指向子类对象。就是多个子类继承一个父类,但是重新修改了分享信息的内容,然后通过同样的对象引用调用同样的方法,但是得出不同的信息。多态性其实还分为编译时多态性和运行时多态性,方法重载实现的是编译时多态性(也称为前绑定),方法重写实现的是运行时多态性(也成为后绑定)。
- 抽象:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面,抽象只关注对象的哪些属性和行为,并不关注这此行为的细节是什么
PS:我还在思考抽象算不算特性!!!!有人说仁者见仁,智者见智。可以是三种也可以是四种
2.==和equals的区别======+2
-
首先,==是等于比较运算符,而equals是object里面的一个方法
-
==对于基本数据类型来说,比较的是值,对于引用数据类型来书,比较的是内存地址
-
equals默认情况下也是比较内存地址,但是我们一般会重写equals使其变为内容比较,即值比较
3.final 在 java 中有什么作用--------被问+1
-
final修饰的类叫最终类,该类不能被继承
-
final修饰的方法不能被重写
-
final修饰的变量叫常量,常量必须初始化,初始化之后的值不能被修改。
package java_interviewtopic_01.stage01;
import java.util.ArrayList;
import java.util.List;
/**
* 3.final
*/
public class Interview03 {
// 使用final修饰的静态类变量需要再声明的时候就赋值或者在静态代码块赋值
final static String i = "I"; // 声明时赋值
// static {
// i = "I"; // 在静态代码块中赋值
// }
// 使用final修饰的类变量需要再声明的时候就赋值或者在代码块赋值和构造函数的赋值
final String j = "J";
// {
// j = "J"; // 在代码块中赋值
// }
// public Interview03(String j) {
// this.j = j; // 在构造函数中赋值
// }
public static void main(String[] args) {
// 局部变量可以先声明后赋值,但是赋值之后也不能再更改了
final String a;
a = "abc";
final List<Integer> list = new ArrayList<>();
list.add(2);
list.add(3);
list.add(4);
list.add(5); // 可以看出内容可以变
// list会爆红
list = new ArrayList<>(); // 可以看出地址不能变了
}
}
PS:当final修饰的变量是一个基本数据类型的时候,这个变量初始化之后的值不能再被更改。但是当final修饰的是一个引用数据类型的时候,该引用的内存地址不能再更改,但是该地址的内容可以改变
4.java 中操作字符串都有哪些类?它们之间有什么区别?===========被问+1
-
String StringBuffer StringBuilder
-
三者共同之处:都是final类,不能被继承
-
StringBuffer与StringBuilder两者共同之处:可以通过append、indert进行字符串的操作。
-
再说这三个类的主要区别:其主要区别在两个方面,即运行速度和线程安全两个方面
-
先说运行速度,在这方面运行速度快慢为:StringBuilder > StringBuffer > String。String慢的原因:因为Stirng为字符常量,而另外两个均为字符串变量。
-
String的课外补充:写个例子:Stirng a = "123" 再将a = a + "45" 这时打印出a = "12345"这个例子看似这个a被更改了。其实不是,这只是一个假象而已。JVM对于这几行代码是这样处理的,首先创建一个String对象a。并把123赋值给a其实在a = a + "45"的时候,JVM又创建了一个新的对象也名为a。然后再把原来的a的值和"45"加起来再赋值给新的a。而原来的a就会被回收机制给回收掉。所以,a实际上并没有被更改,也就是前面说的String对象一旦创建之后不可更改了。
-
Java中对String对象进行的操作实际上是一个不断创建新的对象并且将旧的对象回收的一个过程,所以执行速度很慢。而StringBuilder和StringBuffer的对象是变量,对变量进行操作就是直接对该对象进行更改,而不进行创建和回收的操作,所以速度要比String快很多。
-
再说这个线程安全:StringBuilder是线程不安全的,而StringBuffer是线程安全的
-
如果一个StringBuffer对象在字符串缓冲区被多个线程使用时,StringBuffer中很多方法可以带有synchronized关键字,所以可以保证线程是安全的,但StringBuilder的方法则没有该关键字,所以不能保证线程安全,有可能会出现一些错误的操作。所以如果要进行的操作是多线程的,那么就要使用StringBuffer,但是在单线程的情况下,还是建议使用速度比较快的StringBuilder。
PS:总结一下:
-
String:适用于少量的字符串操作的情况
-
StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况
-
StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况
5.重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分?
-
区别:方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性具体回答:重载发生在同一个类中,方法名一样,但是参数类型不一样,个数不一样,方法返回值和访问修饰符可以不同。重写发生在父子类中,方法名,参数必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。
-
不能根据返回值类型来区分:这个时候如果出现这种情况:
-
float max ( int a , int b );
-
int max ( int a , int b );
-
编译器就不知道该调用哪个了。
6.接口和抽象类有什么区别?=======+1
-
抽象类和接口都不能直接实例化
-
抽象类要被子类继承,接口要被类实现
-
接口只能做方法申明,抽象类中可以做方法申明,也可以做方法实现
-
接口里定义的变量只能是公共的静态的常量,抽象类中的变量是普通变量
-
抽象类中有构造方法,接口类中没有
-
接口和接口之间支持多继承,类和类之间只能单继承
-
接口中只能有抽象方法,而抽象类里可以有方法体方法
-
接口是设计的结果,抽象类是重构的结果。
-
抽象类可以有具体的方法和属性,接口只能有抽象方法和不可变常量
-
抽象类主要用来抽象类别,接口主要用来抽象功能。
-
抽象类可以有 main 方法,并且我们能运行它;接口不能有 main 方法。
-
接口中的方法默认使用 public 修饰;抽象类中的方法可以是任意访问修饰符。(private除外)
-
它们的设计目的不一样,接口的设计目的:是对类的行为进行约束。它只约束行为的有无,不约束你如何去实现行为。抽象类的设计目的是代码的复用。
7.List、Set之间的区别是什么?
-
list集合有序可重复,set无序不可重复,PS:Map以键值对的形式对元素进行存储
-
List允许任意数量的空值,Set最多允许一个空值的出现,PS:Map只允许出现一个空键,但允许出现任意数量的空值
-
List 支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。
-
Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
-
List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变
8.两个对象的 hashCode()相同,则 equals()也一定为 true,对吗?
-
不对
-
因为hashcode方法一般用作于哈希表数据结构,在集合中要存入一个元素的时候,首先调用hashcode方法得出hash值,然后将hash值转换为数组下标,然后拿着该下标去对应的位置,如果该位置没有任何的元素,那么就直接将值存入。如果已经存在改值,那么则进行equals比较,如果返回为false,那么将其存入进去,如果返回为true,则覆盖元素。
-
结论:hashCode()相等的两个对象他们的equal()不一定相等,也就是hashCode()不是绝对可靠的。equals相等的两个对象它们的hashcode一定相等,也就是equals对比是绝对可靠的。
PS:hashCode()相等即两个键值对的哈希值相等,然而哈希值相等。并不一定能得出键值对相等
9.java 中的 Math.round(-5.5) 等于多少?
- 等于-5,在java中,四舍五入的原理是在原参数上加0.5再做向下取整。
10.String str="i"与 String str=new String("i")一样吗?
-
不一样,两个内存分配的方式不一样
-
String str = "i" 的方式java会把它分配到常量池
-
String str = new String("i") 会分配到堆内存中
-
String s = new String("xyz");创建了几个String对象?
-
这个要看情况,当常量池中没有"xyz"的时候,他会先在常量池中创建这个字符串对象,然后再创建这个字符串的引用对象,所以这时候是两个对象。当常量池中有这个"xyz"的时候,就只会创建这个字符串的引用对象,即一个对象
-
ArrayList 和 LinkedList 的区别是什么?--------被问+1
-
ArrayList底层采用动态数组的数据结构,用连续的内存存储,LinkedList是链表的数据结构,可以存储在分散的内存中。
-
ArrayList查询快,插入和删除慢。LinkedList查询慢,插入、删除快
-
随机访问效率不同,
-
HashMap 的实现原理?
-
hashMap的底层采用数组和链表的数据结构。当我们往HashMap里面put元素的时候,底层调用k的hashcode方法得到hash值,然后通过哈希算法将hash值转换为数组的下标,当下标上没有任何元素的时候,就把这个节点放在这个位置上,如果下标对应的位置上有链表,此时会拿着k和链表上的k进行equals比较,如果和所有链表上的k进行equals比较都是返回false,则将其存入末尾,如果有一个返回为true,则将它的值覆盖,如果添加时发现容量不够,就开始扩容。
-
在jdk8版本的时候haspMap在第一次添加数据时,默认构造函数构建的初始容量是16,当达到它的临界值(0.75)的时候,数组就会扩容,如果有一条链表的元素个数到达8,且数组的大小到达64时。就会进化成红黑树,当数组的长度重新低于6的时候,又会将红黑树重新转换为链表。
- PS:题外补充---hashMap的get(k)原理:
-
先调用k的hashcode方法得出hash值,通过哈希算法将hash值转换为数组下标,通过数组下标快速定位,如果该位置上什么都没有,则返回null。如果该位置有链表,那么拿着该k和链表上的所有k进行equals比较,如果所有equals都返回false则返回null,但是只要其中一个节点返回true,则将其value返回。
-
说一下 HashSet 的实现原理?
-
HashSet底层由HashMap实现,扩容机制一样。它封装了一个HashMap来存储所有的集合元素。但是它不一样的是,它的所有集合元素由HashMap的key来保存,而HashMap的value则存储了一个PRESENT。它是一个静态的 Object 对象。
-
HashSet的其它操作原理都是基于HashMap的
-
HashMap 和 Hashtable 有什么区别?
-
HashMap不是线程安全的,而HashTable是线程安全的
-
继承的父类不同
-
HashMap允许null值,key和value都允许,但是hashTable不允许null值,key和value都不允许。
-
两个的遍历方法不同,前者采用Iterator,后者采用Enumeration
-
初始容量和扩容机制不同,Hashtable初始值为11,扩容机制为2n + 1,HashMap初始为16,扩容机制为2n
-
创建时,如果给定了容量初始值,那么Hashtable会直接使用你给定的大小,而HashMap会将其扩充为2的幂次方大小
-
hashMap去掉了HashTable 的contains方法,但是加上了containsValue()和containsKey()方法
-
HashSet与HashMap的区别?
-
HshMap存储键值对,HashSet存储对象
-
HashMap使用put添加元素值Map,HshSet使用add方法
-
计算hashCode值不一样。HashMap使用键对象计算。HashSet使用成员对象来计算
-
效率不一样,HashSet要慢
-
&和&&的区别?======+1
-
&和&&都表示与的意思,既表达式俩边都成立,结果才成立
-
&做逻辑运算符时,左边为假时,它还会计算右边,而&&(短路与)不会,当左边为假时后面则不会计算了
-
&做位运算符时,&的左右俩边可以是布尔类型,也可以是数值,而&&只能是布尔类型
-
字符串连接用+和StringBuilder的append的区别?
-
一般情况下没什么区别,因为一般情况下用+连接,系统内部会进行优化,它用的也是StringBuilder的append来实现的
-
但是在循环拼接的时候,用的如果是+拼接的话,就是一直在循环内部创建StringBuilder对象,这样会造成空间浪费。但是我们直接用StringBuilder的话,可以定义在循环外面。减少内存消耗
// 建议使用
StringBuilder stringBuilder = new StringBuilder("123");
for (int j = 0; j < 10; j++) {
// 这里使用的是外部创建的StringBuilder,就只用一个
stringBuilder.append(j);
}
// 不建议使用
String a = "tt";
for (int j = 0; j < 10; j++) {
// 这里会一直创建StringBuilder,然后进行append。
a += j;
}
- String有哪些特性?
-
不可变,为什么不可变可以查看第四题的课外补充
-
常量池优化:String 对象创建之后,会在字符串常量池中进行缓存,如果下次创建同样的对象时,会直接返回缓存的引用
-
使用 final 来定义 String 类,表示 String 类不能被继承,提高了系统的安全性
-
Integer a= 127 与 Integer b = 127相等吗?那Integer a1 = 128 与 Integer b1 = 128 呢?
-
前者相等,后者不相等
-
因为如果整型字面量的值在-128到127之间,那么自动装箱时不会new新的Integer对象,而是直接引用常量池中的Integer对象,超过这个范围 则会new 新的Integer对象,即a1==b1的结果是比较内存地址,所以不相等
21.this关键字的用法?
-
this是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针
-
this的用法在java中大体可以分为3种:
-
普通的直接引用,this相当于是指向当前对象本身
-
形参与成员名字重名,用this来区分
-
引用本类的构造函数
22.static存在的主要意义?
-
static的主要意义是在于创建独立于具体对象的域变量或者方法。以致于即使没有创建对象也能使用属性和调用方法
-
static关键字还有一个比较关键的作用就是 用来形成静态代码块以优化程序性能。static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候会按照static块的顺序来执行每个static块,并且只会执行一次
23.为什么说static块可以用来优化程序性能?
- 因为它的特性:只会在类加载的时候执行一次。因此,很多时候会将一些只需要进行一次的初始化操作都放在static代码块中进行
24.创建一个对象用什么关键字?对象实例与对象引用有何不同?
-
new关键字
-
new创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。一个对象引用可以指向0个或1个对象一个对象可以有n个引用指向它
25.在Java中定义一个不做事且没有参数的构造方法的作用?---即空构造方法
- Java程序在执行子类的构造方法之前,如果没有用super()来调用父类特定的构造方法,则会调用父类中"没有参数的构造方法"。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用super()来调用父类中特定的构造方法,则编译时将发生错误,因为Java程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法
26.一个类的构造方法的作用是什么?若一个类没有声明构造方法,该程序能正确执行吗?为什么?
-
主要作用是完成对类对象的初始化工作
-
可以执行
-
因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法
27.静态变量和实例变量区别?======+1
-
静态变量: 静态变量由于不属于任何实例对象,属于类的,所以在内存中只会有一份,在类的加载过程中,JVM只为静态变量分配一次内存空间
-
实例变量: 每次创建对象,都会为每个对象分配成员变量内存空间,实例变量是属于实例对象的,在内存中,创建几次对象,就有几份成员变量
28.静态方法和实例方法有何不同?
-
在外部调用静态方法时,可以使用"类名.方法名"的方式,也可以使用"对象名.方法名"的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象
-
静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制
29.对象的相等与指向他们的引用相等,两者有什么不同?
- 对象的相等 比的是内存中存放的内容是否相等而 引用相等 比较的是他们指向的内存地址是否相等
30.java 中 IO 流分为几种?
-
按功能来分:输入流(input)、输出流(output)
-
按功能不同分类: 节点流:包裹源头 处理流:增强功能,提高性能
-
按类型来分:字节流和字符流
-
字节流和字符流的区别是:字节流按 8 位传输以字节为单位输入输出数据
- 字节流: InputStream,OutputStream
-
字符流按 16 位传输以字符为单位输入输出数据
- 字符流: Reader,Writer
31.BIO、NIO、AIO 有什么区别?
-
BIO(同步并阻塞):线程发起IO请求,不管内核是否准备好IO操作,从发起请求起,线程一直阻塞,直到操作完成
-
NIO(同步非阻塞):线程发起IO请求,立即返回;内核在做好IO操作的准备之后,通过调用注册的回调函数通知线程做IO操作,线程开始阻塞,直到操作完成
-
AIO(异步非阻塞):线程发起IO请求,立即返回;内存做好IO操作的准备之后,做IO操作,直到操作完成或者失败, 通过调用注册的回调函数通知线程做IO操作完成或者失败
-
BIO是一个连接一个线程。NIO是一个请求一个线程。AIO是一个有效请求一个线程
32.什么是反射机制?======+1
-
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制
-
简单地说:将类的各个组成部分封装为其他对象
33.反射机制优缺点?
-
优点: 运行期类型的判断,动态加载类,提高代码灵活度。可以解耦,提高程序的可扩展性
-
缺点: 性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情, 性能比直接的java代码要慢很多
34.反射机制的应用场景有哪些?
-
JDBC连接数据库时使用Class.forName()通过反射加载数据库的驱动程序
-
Spring框架也用到很多反射机制,Spring 通过 XML 配置模式装载 Bean 的过程:
-
将程序内所有 XML 或 Properties 配置文件加载入内存中
-
Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息
-
使用反射机制,根据这个字符串获得某个类的Class实例
-
动态配置实例的属性
35.Java获取反射(class)的三种方法
-
Class.forName("全类名"):将字节码文件加载进内存,返回class对像
-
类名.class:通过类名的属性class获取
-
对象.getClass():getClass()方法在Object中定义
36.如何决定使用 HashMap 还是 TreeMap?
-
TreeMap<K,V>的Key值是要求实现java.lang.Comparable,所以迭代的时候TreeMap默认是按照Key值升序排序的,TreeMap的实现是基于红黑树结构。适用于按自然顺序或自定义顺序遍历键(key)
-
HashMap<K,V>的Key值实现散列hashCode(),分布是散列的、均匀的,不支持排序;数据结构主要是数组,链表或红黑树。适用于在Map中插入、删除和定位元素
-
结论:如果你需要得到一个有序的结果时就应该使用TreeMap(因为HashMap中元素的排列顺序是不固定的)。除此之外,由于HashMap有更好的性能,所以大多不需要排序的时候我们会使用HashMap
37.如何实现数组和 List 之间的转换?
- List转换成为数组:调用ArrayList的toArray方法
package com.example.demo.myTest;
import java.util.ArrayList;
import java.util.List;
public class Test01 {
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
strings.add("12");
strings.add("13");
strings.add("14");
strings.add("1");
strings.add("62");
strings.add("52");
String[] strings1 = strings.toArray(new String[strings.size()]);
for (int i = 0; i < strings1.length; i++) {
System.out.println(strings1[i]);
}
System.out.println("****************************");
for (String s : strings1) {
System.out.println(s);
}
}
}
- 数组转换成为List:调用Arrays的asList方法
package com.example.demo.myTest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Test01 {
public static void main(String[] args) {
String[] strings = new String[5];
for (int i = 0; i < 5; i++) {
strings[i] = i + "";
}
List<String> stringList = Arrays.asList(strings);
for (int i = 0; i < stringList.size(); i++) {
System.out.println(stringList.get(i));
}
}
}
38.Array 和 ArrayList 有何区别?
-
Array 数组可以包含基本类型和对象类型,ArrayList 却只能包含对象类型。Array 数组在存放的时候一定是同种类型的元素。ArrayList 就不一定了
-
Array 数组的空间大小是固定的,所以需要使用前确定合适的空间大小。ArrayList 的空间是动态增长的,而且,每次添加新的元素的时候都会检查内部数组的空间是否足够
-
ArrayList 方法上比 Array 更多样化,比如添加全部 addAll()、删除全部 removeAll()、返回迭代器 iterator() 等
39.集合和数组的区别?
-
数组是固定长度的;集合可变长度的
-
数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型
-
数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型
40.怎么确保一个集合不能被修改?
- 可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出 Java. lang. UnsupportedOperationException 异常
41.如何边遍历边移除 Collection 中的元素?
- 边遍历边移除 Collection 的唯一正确方式是使用 Iterator.remove() 方法
42.遍历一个 List 有哪些不同的方式?每种方法的实现原理是什么?Java 中 List 遍历的最佳实践是什么?
-
for 循环遍历,基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后停止
-
迭代器遍历,Iterator。Iterator 是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口。Java 在 Collections 中支持了 Iterator 模式 3.foreach 循环遍历。foreach 内部也是采用了 Iterator 的方式实现,使用时不需要显式声明 Iterator 或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过程中操作数据集合,例如删除、替换 4.最佳实践:Java Collections 框架中提供了一个 RandomAccess 接口,用来标记 List 实现是否支持 Random Access。如果一个数据集合实现了该接口,就意味着它支持 Random Access,按位置读取元素的平均时间复杂度为 O(1),如ArrayList。如果没有实现该接口,表示不支持 Random Access,如LinkedList 5.推荐的做法就是:支持 Random Access 的列表可用 for 循环遍历,否则建议用 Iterator 或 foreach 遍历
43.迭代器 Iterator 是什么?
- 迭代器是一种设计模式,它是一个对象,它可以遍历并选择序列中的对象,但是该对象比较特殊,不能直接创建对象,该对象是以内部类的形式存在于每个集合类的内部。迭代器通常被称为"轻量级"对象,因为创建它的代价小
44.Iterator 怎么使用?有什么特点?
-
iterator只能单向移动。使用方法iterator()要求容器返回一个Iterator。第一次调用Iterator的next()方法时,它返回序列的第一个元素。注意:iterator()方法是java.lang.Iterable接口,被Collection继承。使用next()获得序列中的下一个元素。
-
terator在遍历元素过程中,有线程修改集合元素会有ConcurrentModificationEception异常
45.Iterator 和 ListIterator 的区别?
-
Iterator可用来遍历Set和List集合,但是ListIterator只能用来遍历List,而Set并不能使用ListIterator
-
ListIterator有add()方法,可以向List中添加对象,而Iterator不能
-
ListIterator和Iterator都有hasNext()和next()方法,可以实现顺序向后遍历。但是ListIterator有hasPrevious()和previous()方法,可以实现逆向(顺序向前)遍历。Iterator就不可以
-
ListIterator可以定位当前的索引位置,nextIndex()和previousIndex()可以实现。Iterator 没有此功能
-
都可实现删除对象,但是ListIterator可以实现对象的修改,set()方法可以实现。Iterator仅能遍历,不能修改。因为ListIterator的这些功能,可以实现对LinkedList等List数据结构的操作
46.说一下 ArrayList 的优缺点?
-
优点:(1)ArrayList 底层以数组实现,是一种随机访问模式。ArrayList 实现了 RandomAccess 接口,因此查找的时候非常快.(2)ArrayList在顺序添加一个元素的时候非常方便
-
缺点:(1)删除元素的时候,需要做一次元素复制操作。如果要复制的元素很多,那么就会比较耗费性能.(2)插入元素的时候,也需要做一次元素复制操作,缺点同上
-
结论:ArrayList 比较适合顺序添加、随机访问的场景
47.多线程场景下如何使用 ArrayList?
- ArrayList 不是线程安全的,如果遇到多线程场景,可以通过 Collections 的 synchronizedList 方法将其转换成线程安全的容器后再使用
package com.company.test01;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Test05 {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
// 主要是这个方法
List<String> list1 = Collections.synchronizedList(list);
list1.add("12");
list1.add("13");
list1.add("14");
list1.add("15");
list1.add("16");
list1.add("17");
for (int i = 0; i < list1.size(); i++) {
System.out.println(list1.get(i));
}
}
}
48.为什么 ArrayList 的 elementData 加上 transient 修饰?
- ArrayList 实现了 Serializable 接口,这意味着 ArrayList 支持序列化。transient 的作用是说不希望 elementData 数组被序列化.重写了 writeObject 实现:每次序列化时,先调用 defaultWriteObject() 方法序列化 ArrayList 中的非 transient 元素,然后遍历 elementData,只序列化已存入的元素,这样既加快了序列化的速度,又减小了序列化之后的文件大小
49.HashSet如何检查重复?HashSet是如何保证数据不可重复的?======+1
-
检查重复:向HashSet 中add ()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equles 方法比较。HashSet 中的add ()方法会使用HashMap 的put()方法
-
保证数据不重复:HashMap 的 key 是唯一的,由源码可以看出 HashSet 添加进去的值就是作为HashMap 的key,并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回新的V。所以不会重复( HashMap 比较key是否相等是先比较hashcode 再比较equals )
50.为什么HashMap中String、Integer这样的包装类适合作为K?
-
String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率
-
都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况
-
内部已重写了equals()、hashCode()等方法,遵守了HashMap内部的规范,不容易出现Hash值计算错误的情况
51.如果使用Object作为HashMap的Key,应该怎么办呢?
- 重写hashCode和equals方法
52.HashSet和TreeSet分别如何实现去重的?
-
HashSet:添加元素时,先得到hash值,会转成索引值,找到存储数据表table,看这个索引位置是否有元素,如果没有,则直接加入。如果有元素,即调用equals比较(记得要重写equals),如果相同,就放弃添加,如果不相同,就将其放到最后。如果添加时发现容量不够,开始扩容。
-
TreeSet去重机制:如果你传入的一个Comparator匿名对象,就使用实现的comparator去重,如果方法返回0,就认为是相同的元素,就不添加,如果你没有传入一个Comparator匿名对象,则以你添加的对象实现的Comparable接口里的compareTo方法去重
53.创建一个User类,TreeSet treeSet = new TreeSet();treeSet.add(new User);会不会报错?为什么?
- 首先会不会报错取决于你的这个User类有没有实现Comparable接口,如果没有去实现这个接口,那么在TreeSet使用add方法的时候,它的底层会去将User类转成Comparable类型。这时候,你没实现这个接口,那么会报出一个类型异常的错误
54.java创建对象有几种方式?========+1
-
通过new关键字创建对象
-
通过反射创建对象
-
通过clone创建对象
-
通过序列化创建对象
55.java的克隆实现方式以及理解?克隆针对的是类还是对象?======+1
-
克隆的对象必须实现Cloneable这个接口,而且需要重写clone方法,重写时将该方法由受保护变为公开。 一个实现了Cloneable类意味着可以通过java.lang.Object的clone()合法的对该类的实例的属性逐一复制。没有实现Cloneable接口的类的实例调用clone()会抛出CloneNotSupportedException。克隆针对的是对象!
-
基本数据类型克隆时只是传值,不存在传引用。没有实现Cloneable接口的类的实例克隆是通过传引用实现的,String这个情况特殊,理解String不可变原理即可。可以看看这篇文章
-
浅克隆:
package com.company.test01;
public class TestClone {
public static void main(String[] args) throws CloneNotSupportedException {
TestA testA = new TestA(1, "1437");
Test test = new Test();
test.setName("13");
test.setAge(19);
test.setTestA(testA);
// 克隆test
Test test1 = test.clone();
System.out.println(test1);
System.out.println(test);
// 修改test1的值观看变化
test1.setAge(21);
test1.setName("14");
test1.getTestA().setId(2);
test1.getTestA().setName("201437");
System.out.println(test1);
System.out.println(test);
}
}
class Test implements Cloneable{
private String name;
private int age;
private TestA testA;
public Test() {
}
public Test(String name, int age, TestA testA) {
this.name = name;
this.age = age;
this.testA = testA;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public TestA getTestA() {
return testA;
}
public void setTestA(TestA testA) {
this.testA = testA;
}
@Override
public String toString() {
return "Test{" +
"name='" + name + '\'' +
", age=" + age +
", testA=" + testA +
'}';
}
@Override
protected Test clone() throws CloneNotSupportedException {
Test test = null;
test = (Test)super.clone();
return test;
}
}
class TestA{
private int id;
private String name;
public TestA() {
}
public TestA(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "TestA{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
// 打印结果--可以看到,Test对象的name属性并没有变化,但是age属性和testA对象的值是跟着一起变化了
// 这里打印的是没有修改任何值的时候
Test{name='13', age=19, testA=TestA{id=1, name='1437'} }
Test{name='13', age=19, testA=TestA{id=1, name='1437'} }
// 这里打印的是test1修改值之后
Test{name='14', age=21, testA=TestA{id=2, name='201437'} }
Test{name='13', age=19, testA=TestA{id=2, name='201437'} }
- 深克隆:
package com.company.test01;
public class TestCloneDeep {
public static void main(String[] args) throws CloneNotSupportedException {
TestDeepB testDeepB = new TestDeepB(21, "581437");
TestDeepA testDeepA = new TestDeepA();
testDeepA.setAge(18);
testDeepA.setName("1437");
testDeepA.setTestDeepB(testDeepB);
// 进行克隆
TestDeepA testDeepA1 = (TestDeepA) testDeepA.clone();
System.out.println(testDeepA);
System.out.println(testDeepA1);
// 修改testDeepA1的值
testDeepA1.setAge(50);
testDeepA1.setName("201437");
testDeepA1.getTestDeepB().setAge(51);
testDeepA1.getTestDeepB().setName("58143700");
// 修改完后打印
System.out.println(testDeepA);
System.out.println(testDeepA1);
}
}
class TestDeepA implements Cloneable {
private int age;
private String name;
private TestDeepB testDeepB;
public TestDeepA() {
}
public TestDeepA(int age, String name, TestDeepB testDeepB) {
this.age = age;
this.name = name;
this.testDeepB = testDeepB;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public TestDeepB getTestDeepB() {
return testDeepB;
}
public void setTestDeepB(TestDeepB testDeepB) {
this.testDeepB = testDeepB;
}
@Override
public String toString() {
return "TestDeepA{" +
"age=" + age +
", name='" + name + '\'' +
", testDeepB=" + testDeepB +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
// 这里实现第一步,只是一个浅克隆
TestDeepA testDeepA = null;
testDeepA = (TestDeepA)super.clone();
// 这里使用双层克隆,让TestDeepB也实现克隆
testDeepA.setTestDeepB((TestDeepB)testDeepA.getTestDeepB().clone());
return testDeepA;
}
}
class TestDeepB implements Cloneable {
private int age;
private String name;
public TestDeepB() {
}
public TestDeepB(int age, String name) {
this.age = age;
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "TestDeepB{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
TestDeepB testDeepB = null;
testDeepB = (TestDeepB)super.clone();
return testDeepB;
}
}
// 打印结果--可以看到,只是克隆的那个值发生了变化
TestDeepA{age=18, name='1437', testDeepB=TestDeepB{age=21, name='581437'} }
TestDeepA{age=18, name='1437', testDeepB=TestDeepB{age=21, name='581437'} }
TestDeepA{age=18, name='1437', testDeepB=TestDeepB{age=21, name='581437'} }
TestDeepA{age=50, name='201437', testDeepB=TestDeepB{age=51, name='58143700'} }
- 利用serializable实现深复制
package com.company.test01;
import java.io.*;
public class TestCloneDeep01 {
public static void main(String[] args) throws Exception {
TestDeep01B testDeep01B = new TestDeep01B("1437", 18);
TestDeep01A testDeep01A = new TestDeep01A("201437", 28, testDeep01B);
// 克隆
TestDeep01A testDeep01A1 = CloneUtils.clone(testDeep01A);
// 打印
System.out.println(testDeep01A);
System.out.println(testDeep01A1);
// 修改testDeep01A1数据
testDeep01A1.setName("581437");
testDeep01A1.setAge(58);
testDeep01A1.getTestDeep01B().setName("13");
testDeep01A1.getTestDeep01B().setAge(60);
// 修改完打印
System.out.println(testDeep01A);
System.out.println(testDeep01A1);
}
}
class TestDeep01A implements Serializable {
private static final long UID = -3936148364278781437L;
private String name;
private int age;
private TestDeep01B testDeep01B;
public TestDeep01A() {
}
public TestDeep01A(String name, int age, TestDeep01B testDeep01B) {
this.name = name;
this.age = age;
this.testDeep01B = testDeep01B;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public TestDeep01B getTestDeep01B() {
return testDeep01B;
}
public void setTestDeep01B(TestDeep01B testDeep01B) {
this.testDeep01B = testDeep01B;
}
@Override
public String toString() {
return "TestDeep01A{" +
"name='" + name + '\'' +
", age='" + age + '\'' +
", testDeep01B=" + testDeep01B +
'}';
}
}
class TestDeep01B implements Serializable {
private static final long UID = -4482468804013491437L;
private String name;
private int age;
public TestDeep01B() {
}
public TestDeep01B(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "TestDeep01B{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
// 工具类
class CloneUtils {
public static <T extends Serializable> T clone(T o) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(o);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
return (T)objectInputStream.readObject();
}
}
// 打印结果
TestDeep01A{name='201437', age=28, testDeep01B=TestDeep01B{name='1437', age=18} }
TestDeep01A{name='201437', age=28, testDeep01B=TestDeep01B{name='1437', age=18} }
TestDeep01A{name='201437', age=28, testDeep01B=TestDeep01B{name='1437', age=18} }
TestDeep01A{name='581437', age=58, testDeep01B=TestDeep01B{name='13', age=60} }
56.java的参数传递方式是什么?=======+2
-
java使用的是值传递。
-
java中只有值传递,基本类型传递的是值的副本,引用类型传递的是引用的副本。可以看看这个文章
57.synchronized 的作用? ======+1
- 在 Java 中,synchronized 关键字是用来控制线程同步的,就是在多线程的环境下,控制 synchronized 代码段不被多个线程同时执行;synchronized 可以修饰类、方法、变量。
58.synchronized 和 Lock 有什么区别?======+1
-
首先synchronized是Java内置关键字,在JVM层面,Lock是个Java类
-
synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁
-
synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁
-
通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到
59.jdk1.8对hashMap做了哪些优化?======+1
-
最重要的一点是底层结构不一样,1.7是数组+链表,1.8则是数组+链表+红黑树结构
-
插入键值对的put方法的区别,1.8中会将节点插入到链表尾部,而1.7中是采用头插;
-
扩容机制:1.7中是只要不小于阈值就直接扩容2倍;在jdk8版本的时候haspMap在第一次添加数据时,默认构造函数构建的初始容量是16,当达到它的临界值(0.75)的时候,数组就会扩容,如果有一条链表的元素个数到达8,且数组的大小到达64时。就会进化成红黑树,当数组的长度重新低于6的时候,又会将红黑树重新转换为链表
60.java多线程的实现方式?======+2
- 继承Thread并重写run方法,调用start方法
-
继承Thread类是最常见的实现方式之一。可以通过创建一个继承自Thread类的子类,并重写run()方法来实现多线程的逻辑。重写run()方法后,需要创建该子类的实例,并通过调用start()方法来启动线程。此时,该实例的run()方法将在新线程中执行。例如:
-
PS: 需要注意的是,不能直接调用子类的run()方法来启动线程,因为这样会在当前线程中执行run()方法,而不是在新线程中执行
package multithreading;
public class TestThread {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread thread = new Test01();
thread.start();
}
}
}
class Test01 extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
- 实现Runnable接口,并用其初始化Thread,然后创建Thread实例,调用start方法
-
除了继承Thread类,还可以实现Runnable接口来实现多线程。可以创建一个实现了Runnable接口的类,并在该类中实现run()方法。然后,在创建Thread对象时,将该类的实例作为参数传入即可,然后调用start方法。
-
PS:需要注意的是,实现Runnable接口只是定义了一个任务,而不是线程。在创建Thread对象时,需要将该任务传递给Thread构造函数。
-
和继承Thread类相比,实现Runnable接口有以下优点: 可以避免Java单继承的限制。 可以将任务从线程控制分离出来。
package multithreading;
public class TestRunnable {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(new Test02());
thread.start();
}
}
}
class Test02 implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
- 实现Callable接口,并用其初始化Thread,然后创建Thread实例,调用start方法
-
实现Callable接口时,需要重写call()方法,并在里面编写线程的业务逻辑
-
创建一个ExecutorService类型的线程池,使用newFixedThreadPool()方法创建一个指定大小的线程池。例如代码:
-
将Callable对象提交到线程池中执行,并获取Future对象。例如代码
-
在需要时通过Future对象获取线程执行结果。例如:
-
最后切记要关闭线程池
package multithreading;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class TestCallable {
public static void main(String[] args) throws Exception {
for (int i = 0; i < 10; i++) {
//创建Test03实例
Callable<Boolean> callable1 = new Test03();
Callable<Boolean> callable2 = new Test03();
Callable<Boolean> callable3 = new Test03();
// 创建执行服务
ExecutorService executorService = Executors.newFixedThreadPool(3);
// 提交执行
Future<Boolean> submit1 = executorService.submit(callable1);
Future<Boolean> submit2 = executorService.submit(callable2);
Future<Boolean> submit3 = executorService.submit(callable3);
// 获取结果
Boolean aBoolean1 = submit1.get();
Boolean aBoolean2 = submit2.get();
Boolean aBoolean3 = submit3.get();
System.out.println(aBoolean1);
System.out.println(aBoolean2);
System.out.println(aBoolean3);
// 关闭服务
executorService.shutdownNow();
}
}
}
class Test03 implements Callable<Boolean> {
@Override
public Boolean call() throws Exception {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "c:" + i);
}
return true;
}
}
- 使用线程池创建
-
Java提供了Executors类来创建线程池,常用的有newFixedThreadPool()、newCachedThreadPool()和newSingleThreadExecutor()等方法。例如,创建一个固定大小的线程池:具体如下代码
-
创建Runnable或Callable任务需要执行的任务可以是实现了Runnable接口或Callable接口的类。例如:
-
提交任务到线程池,使用submit()方法将任务提交到线程池中。例如
-
关闭线程池
-
PS:需要注意的是,在使用带有返回值的线程时,还可以通过isDone()方法来判断目标线程是否已经执行完毕
import java.util.concurrent.*;
public class TestThreadPool {
public static void main(String[] args) throws Exception {
// 创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(3);
// 提交任务到线程池
Runnable myRunnable = new MyRunnable();
Future<?> future1 = executorService.submit(myRunnable);
Callable<String> myCallable = new MyCallable();
Future<String> future2 = executorService.submit(myCallable);
// 获取任务执行结果
Object result1 = future1.get();
String result2 = future2.get();
// 关闭线程池
executorService.shutdown();
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
// 线程的业务逻辑代码
}
}
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
// 线程的业务逻辑代码
return "线程执行完毕";
}
}
61.while循环和for循环的区别?======+1
62.break,continue,return,有什么区别?=======+2
PS:这个问题很奇怪,这么简单的问题为什么会被问两次
63.悲观锁和乐观锁的区别?
1.悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其他线程阻塞,用完后在把资源转让给其他线程) 2.总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现 3.乐观锁常见的两种实现方式:
-
版本号机制:
- 一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会+1.当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库的version值相等时才更新,否则重试更新操作,直到更新成功
-
使用时间戳:跟版本号一样的逻辑
PS:还有一种CAS方法解决