一、Java基础(一)
1、一个Java文件可以有多个类,但最多只能有一个被public修饰。
2、如果这个Java文件中包含public修饰的类,则这个类名必须和Java文件名一致。
3、JVM是Java跨平台的关键(JVM是由C/C++开发完成),跨平台的是Java程序不是jvm
4、编译结果生成字节码、不是机器码,字节码不能直接运行,必须通过JVM翻译成机器码才可以运行
5、Java包含三种修饰符,private、protected、public,四种访问权限(default)
在修饰成员变量/成员方法时,该成员的四种访问权限的含义如下:
private:该成员可以被该类内部成员访问;
default:该成员可以被该类内部成员访问,也可以被同一包下其他的类访问;
protected:该成员可以被该类内部成员访问,也可以被同一包下其他的类访问,还可以被它的子类访问;
public:该成员可以被任意包下,任意类的成员进行访问。
在修饰类时,该类只有两种访问权限,对应的访问权限的含义如下:
default:该类可以被同一包下其他的类访问;
6、Java数据类型byte(1)、short(2)、int(4)、long(8)、float(4)、double(8)、char(2)、boolean int范围(-2^31~ 2^31-1)
7、全局变量(成员变量)和局部变量:全局变量在类中定义的变量,它在整个类中都有效。作为方法或语句块的成员而存在,存在于方法的参数列表和方法定义中。局部变量必须先初始化才可以使用。
8、Java语言是面向对象的语言,其设计理念是“一切皆对象”。但8种基本数据类型却出现了例外,它们不具备对象的特性。正是为了解决这个问题,Java为每个基本数据类型都定义了一个对应的引用类型,这就是包装类。
9、自动装箱:可以把一个基本类型的数据直接赋值给对应的包装类型;
自动拆箱:可以把一个包装类型的对象直接赋值给对应的基本类型;
10、不同数据类型怎么比较,如Integer和Double类型,可以将其转化为相同的基本类型(double)再进行比较。
二、Java基础(二)
1、int是基本数据类型,Integer是包装类。在用==做运算时,Integer会自动拆箱。
2、面向对象的理解:面向对象是一种更优秀的程序设计方法,它的基本思想是使用类、对象、继承、封装、消息等基本概念进行程序设计
3、面向对象的三大特征:封装、继承、多态。封装指的是将对象的实现细节隐藏起来,然后通过一些公用方法来暴露该对象的功能;继承是面向对象实现软件复用的重要手段,当子类继承父类后,子类作为一种特殊的父类,将直接获得父类的属性和方法;多态指的是子类对象可以直接赋给父类变量,但运行时依然表现出子类的行为特征,这意味着同一个类型的对象在执行同一个方法时,可能表现出多种行为特征。(抽象也是面向对象的重要部分,抽象就是忽略一个主题中与当前目标无关的那些方面,以便更充分地注意与当前目标有关的方面)
4、封装的目的:封装是面向对象编程语言对客观世界的模拟,在客观世界里,对象的状态信息都被隐藏在对象内部,外界无法直接操作和修改。对一个类或对象实现良好的封装,可以实现以下目的:
- 隐藏类的实现细节;
- 让使用者只能通过事先预定的方法来访问数据,从而可以在该方法里加入控制逻辑,限制对成员变量的不合理访问;
- 可进行数据检查,从而有利于保证对象信息的完整性;
- 便于修改,提高代码的可维护性。
(封装实际上有两个方面的含义:把该隐藏的隐藏起来,把该暴露的暴露出来)
5、多态:因为子类其实是一种特殊的父类,因此Java允许把一个子类对象直接赋给一个父类引用变量,无须任何类型转换,或者被称为向上转型,向上转型由系统自动完成。
当把一个子类对象直接赋给父类引用变量时,例如 BaseClass obj = new SubClass();,这个obj引用变量的编译时类型是BaseClass,而运行时类型是SubClass,当运行时调用该引用变量的方法时,其方法行为总是表现出子类方法的行为特征,而不是父类方法的行为特征,这就可能出现:相同类型的变量、调用同一个方法时呈现出多种不同的行为特征,这就是多态。
6、多态实现:多态的实现离不开继承,在设计程序时,我们可以将参数的类型定义为父类型。在调用程序时,则可以根据实际情况,传入该父类型的某个子类型的实例,这样就实现了多态。对于父类型,可以有三种形式,即普通的类、抽象类、接口。对于子类型,则要根据它自身的特征,重写父类的某些方法,或实现抽象类/接口的某些抽象方法。
7、Java只可以单继承:Java语言之所以摒弃了多继承的这项特征,是因为多继承容易产生混淆。比如,两个父类中包含相同的方法时,子类在调用该方法或重写该方法时就会迷惑。(间接继承多个父类)
8、重载和重写:重载(方法名相同、参数列表不同)发生在同一个类中,重写(方法名、参数列表必须与父类方法相同)发生在父类子类中。
9、构造方法为啥不能重写:因为构造方法需要和类保持同名,而重写的要求是子类方法要和父类方法保持同名
三、Java基础(三)
1、hashCode和equals区别:hashCode用于获取哈希码,equals用于比较两个对象是否相等,对象相等哈希码必相等。(HashSet存储)
2、序列化和反序列化:把对象转换成字节序列化称为对象序列化,把字节序列恢复为对象的过程称为反序列化(当你想把的内存中的对象状态保存到一个文件中或者数据库中时候;当你想用套接字在网络上传送对象的时候;当你想通过RMI传输对象的时候;)
3、为什么重写equals和hashCode方法:Object类提供的equals()方法默认是用==来进行比较的,也就是说只有两个对象是同一个对象时,才能返回相等的结果。而实际的业务中,我们通常的需求是,若两个不同的对象它们的内容是相同的,就认为它们相等。
4、==运算符:
- 作用于基本数据类型时,是比较两个数值是否相等;
- 作用于引用数据类型时,是比较两个对象的内存地址是否相同,即判断它们是否为同一个对象;
equals()方法:
- 没有重写时,Object默认以 == 来实现,即比较两个对象的内存地址是否相同;
- 进行重写后,一般会按照对象的内容来进行比较,若两个对象内容相同则认为对象相等,否则认为对象不等。
5、String类常用方法:substring(int beginIndex,int endIndex); 如”abcdef“ (0,3) “abc”
charAt(int index) 获取字符
6、String可以被继承吗:String类由final修饰,所以不能被继承。Java的初衷,String类被设计为不可变类。出于安全和性能的考虑,Java中String会用来存储敏感信息,如账号,密码,网络路径,文件处理等场景里,保证字符串 String 类的安全性就尤为重要了,如果字符串是可变的,容易被篡改。由于 String 天然的不可变,当一个线程”修改“了字符串的值,只会产生一个新的字符串对象,不会对其他线程的访问产生副作用,访问的都是同样的字符串数据,不需要任何同步操作。
(类中所有 private 方法都隐式地指定为 final 的,final 方法是可以被重载的,因为static关键字所修饰的字段并不属于一个对象,而是属于这个类的)
7、String和StringBuffer区别:String类是不可变类,即一旦一个String对象被创建以后,包含在这个对象中的字符序列是不可改变的,直至这个对象被销毁。StringBuffer对象则代表一个字符序列可变的字符串,当一个StringBuffer被创建以后,通过StringBuffer提供的append()、insert()、reverse()、setCharAt()、setLength()等方法可以改变这个字符串对象的字符序列。
8、StringBuffer和StringBuilder区别:StringBuffer、StringBuilder都代表可变的字符串对象,它们有共同的父类 AbstractStringBuilder,并且两个类的构造方法和成员方法也基本相同。不同的是,StringBuffer是线程安全的,而StringBuilder是非线程安全的,所以StringBuilder性能略高。一般情况下,要创建一个内容可变的字符串,建议优先考虑StringBuilder类。
9、使用字符串时,new和"“推荐使用哪种方式:推荐”",因为new会多创建一个对象,会占用更多的内存。
10、字符串拼接 “+”、StringBuilder自带方法、StringBuffer自带方法、String中的concat。
11、String a = “abc”; ,说一下这个过程会创建什么,放在哪里。Java使用常量池来管理字符串直接量。在执行这句话时,JVM会先检查常量池中有没有这个字符串,若有,则复用常量池中已有的,将其引用赋值给变量a;
12、new String(“abc”) 是去了哪里,仅仅是在堆里面吗:在执行这句话时,JVM会先使用常量池来管理字符串直接量,即将"abc"存入常量池。然后再创建一个新的String对象,这个对象会被保存在堆内存中。并且,堆中对象的数据会指向常量池中的直接量。
13、接口体现的是一种规范,抽象类体现的是一种模板式设计。
14、接口体现的是一种规范和实现分离的设计哲学,充分利用接口可以极好地降低程序各模块之间的耦合,从而提高系统的可扩展性和可维护性。基于这种原则,很多软件架构设计理论都倡导“面向接口”编程,而不是面向实现类编程,希望通过面向接口编程来降低程序的耦合。
四、Java基础(四)
1、异常处理:三步骤,捕获异常try、处理异常catch、回收资源finally
2、try用于包裹业务代码、catch用于捕获并处理某个类型的异常、finally用于回收资源
3、finally中使用return和throw语句就会导致try块和catch块中的return、throw语句失效。
4、static关键字的作用是把类的成员变成类相关,而不是实例相关。
5、被 final 修饰的任何形式的变量,一旦获得了初始值,就不可以被修改!
6、泛型:Java集合有一个缺点就是,把一个对象丢进集合之后,集合就不知道这个对象的数据类型,当再次取出时,对象的编译类型就变成了Object类型(其运行时类型没变)。两个问题。从Java 5开始,Java引入了“参数化类型”的概念,允许程序在创建集合时指定集合元素的类型,Java的参数化类型被称为泛型(Generic)。例如 List,表明该List只能保存字符串类型的对象。有了泛型以后,程序再也不能“不小心”地把其他对象“丢进”集合中。而且程序更加简洁,集合自动记住所有集合元素的数据类型,从而无须对集合元素进行强制类型转换。
7、泛型擦除:
List < String> list1 = …; List list2 = list1; // list2将元素当做Object处理
8、Java反射机制:Java程序中的对象在运行时可以表现为两种类型,即编译时类型和运行时类型。例如 Person p = new Student(); ,这行代码将会生成一个p变量,该变量的编译时类型为Person,运行时类型为Student。
9、Java反射机制运用场景:
- 使用JDBC时,如果要创建数据库的连接,则需要先通过反射机制加载数据库的驱动程序;
- 多数框架都支持注解/XML配置,从配置中解析出来的类是字符串,需要利用反射机制实例化;
- 面向切面编程(AOP)的实现方案,是在程序运行时创建目标对象的代理类,这必须由反射机制来实现。
五、集合类(一)
1、Java中的集合类主要由Collection和Map这两个接口派生而出,其中Collection接口又派生出三个子接口,分别是Set、List、Queue。所有的Java集合类,都是Set、List、Queue、Map这四个接口的实现类。Set代表无序的,元素不可重复。List代表有序的,元素可以重复。Queue代表先进先出的队列。Map代表具有映射关系的集合。常用的集合类有:HashSet、TreeSet、ArrayList、LinkedList、ArrayQueue、HashMap、TreeMap等。
注:紫色框体代表接口,其中加粗的是代表四类集合的接口。蓝色框体代表实现类,其中有阴影的是常用实现类。
2、Java中线程安全和线程不安全的容器有哪些:java.util包下的集合类大部分都是线程不安全的,例如我们常用的HashSet、TreeSet、ArrayList、LinkedList、ArrayQueue、HashMap、TreeMap,这些都是线程不安全的集合类,但是性能好。如果需要线程安全的集合类,则可以使用Collections工具类提供的synchronizedXxx()方法,将这些集合包装成线程安全的集合类。Java.util下的Vector、HashTable就是线程安全的,但是性能很差。
java.util.concurrent包下线程安全的集合类的体系结构:
3、Map接口常见实现类:HashMap、TreeMap、ConcurrentHashMap。对于需要排序的,按照插入顺序排序的首选LinkedHashMap,对于不需要排序的首选HashMap。如果需要线程安全,可以使用Collections工具类将上述实现类包装成线程安全的Map。
4、Map.put过程:
HashMap是最经典的Map实现,下面以它的视角介绍put的过程:
1.首次扩容:
先判断数组是否为空,若数组为空则进行第一次扩容(resize)
2.计算索引:
通过hash算法,计算键值对在数组中的索引;
3.插入数据:
如果当前位置元素为空,则直接插入数据;
如果当前位置元素非空,且key已存在,则直接覆盖其value;
如果当前位置元素非空,且key不存在,则将数据链到链表末端;
若链表长度达到8,则将链表转换成红黑树,并将数据插入树中;
4.再次扩容
如果数组中元素个数(size)超过threshold,则再次进行扩容操作。
5、怎么得到线程安全的Map:使用Collections工具类,将线程不安全的Map包装成线程安全的、使用java.util.concurrent包下的Map,如ConcurrentHashMap、不建议使用Hashtble,虽然线程安全但是性能差。
6、HashMap特点:HashMap线程不安全、可以使用null作为key和value。
7、JDK7中的HashMap,是基于数组+链表来实现的,它的底层维护一个Entry数组。它会根据计算的hashCode将对应的KV键值对存储到该数组中,一旦发生hashCode冲突,那么就会将该KV键值对放到对应的已有元素的后面, 此时便形成了一个链表式的存储结构。JDK7中HashMap的实现方案有一个明显的缺点,即当Hash冲突严重时,在桶上形成的链表会变得越来越长,这样在查询时的效率就会越来越低,其时间复杂度为O(N)。JDK8中的HashMap,是基于数组+链表+红黑树来实现的,它的底层维护一个Node数组。当链表的存储的数据个数大于等于8的时候,不再采用链表存储,而采用了红黑树存储结构。这么做主要是在查询的时间复杂度上进行优化,链表为O(N),而红黑树一直是O(logN),可以大大的提高查找性能。
8、HashMap底层实现原理:它基于hash算法,通过put方法和get方法存储和获取对象。
9、HashMap扩容机制:
1. 数组的初始容量为16,而容量是以2的次方扩充的,一是为了提高性能使用足够大的数组,二是为了能使用位运算代替取模预算(据说提升了5~8倍)。
2. 数组是否需要扩充是通过负载因子判断的,如果当前元素个数为数组容量的0.75时,就会扩充数组。这个0.75就是默认的负载因子,可由构造器传入。
我们也可以设置大于1的负载因子,这样数组就不会扩充,牺牲性能,节省内存。
3. 为了解决碰撞,数组中的元素是单向链表类型。当链表长度到达一个阈值时(7或8),会将链表转换成红黑树提高性能。而当链表长度缩小到另一个
阈值时(6),又会将红黑树转换回单向链表提高性能。
4. 对于第三点补充说明,检查链表长度转换成红黑树之前,还会先检测当前数组数组是否到达一个阈值(64),如果没有到达这个容量,会放弃转换,
先去扩充数组。所以上面也说了链表长度的阈值是7或8,因为会有一次放弃转换的操作。
10、HashMap为什么使用红黑树而不是B树:B/B+树多用于外存上时,B/B+也被成为一个磁盘友好的数据结构。HashMap本来是数组+链表的形式,链表由于其查找慢的特点,所以需要被查找效率更高的树结构来替换。如果使用B/B+树的话,在数量不是很多的情况下,数据都会"挤在"一个结点里面,这个时候遍历效率就退化成了链表。
11、HashMap为什么线程不安全:HasnMap在并发执行put操作时,可能会导致形成环形链表,从而引起死循环。
12、HashMap如何实现线程安全:直接使用Hashtable、直接使用 ConcurrentHashMap、使用Collections将HashMap包装成安全的Map
13、HashMap怎么解决哈希冲突:为了解决碰撞,数组中的元素是单向链表类型。当链表长度达到一个阈值时,会将链表转换成红黑树提高性能。而当链表长度缩小到另一个阈值,又会将红黑树转换回单向链表提高性能。
14、HashMap和Hashtable区别:HashMap是一个线程不安全的Map实现,Hashtable是一个线程安全的Map实现,所以HashMap比HashTable性能高一点。HashTable不允许null作为key和value。
15、HashMap和ConcurrentHashMap:HashMap是非线程安全的,不可以在多个线程中对Map修改,不然会产生数据不一致,甚至会因并发插入元素导致链表成环,在查找时就会发生死循环。Collections工具类使用synchronized关键字保证线程安全(Hashtable也是使用synchronized关键字)。ConcurrentHashMap未使用全局锁,采用减少锁粒度的方法,尽量减少因竞争锁导致的阻塞和冲突,ConcurrentHaashMap的检索操作是不需要锁的。
16、ConcurrentHashMap实现:在 jdk 1.7 中,ConcurrentHashMap 是由 Segment 数据结构和 HashEntry 数组结构构成,采取分段锁来保证安全性。Segment 是 ReentrantLock 重入锁,在 ConcurrentHashMap 中扮演锁的角色,HashEntry 则用于存储键值对数据。一个 ConcurrentHashMap 里包含一个 Segment 数组,一个 Segment 里包含一个 HashEntry 数组,Segment 的结构和 HashMap 类似,是一个数组和链表结构。JDK1.8 的实现已经摒弃了 Segment 的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 Synchronized 和 CAS 来操作,整个看起来就像是优化过且线程安全的 HashMap,虽然在 JDK1.8 中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本。
17、ConcurrentHashMap分段分组:
get操作:
Segment的get操作实现非常简单和高效,先经过一次再散列,然后使用这个散列值通过散列运算定位到 Segment,再通过散列算法定位到元素。
get操作的高效之处在于整个get过程都不需要加锁,除非读到空的值才会加锁重读。原因就是将使用的共享变量定义成 volatile 类型。
put操作:
当执行put操作时,会经历两个步骤:
判断是否需要扩容;
定位到添加元素的位置,将其放入 HashEntry 数组中。
插入过程会进行第一次 key 的 hash 来定位 Segment 的位置,如果该 Segment 还没有初始化,即通过 CAS 操作进行赋值,然后进行第二次 hash
操作,找到相应的 HashEntry 的位置,这里会利用继承过来的锁的特性,在将数据插入指定的 HashEntry 位置时(尾插法),会通过继承
ReentrantLock的 tryLock() 方法尝试去获取锁,如果获取成功就直接插入相应的位置,如果已经有线程获取该Segment的锁,那当前线程会以自旋
的方式去继续的调用tryLock() 方法去获取锁,超过指定次数就挂起,等待唤醒。
18、LinkedHashMap:
- LinkedHashMap使用双向链表来维护key-value对的顺序(其实只需要考虑key的顺序),该链表负责维护Map的迭代顺序,迭代顺序与key-value对的插入顺序保持一致。
- LinkedHashMap可以避免对HashMap、Hashtable里的key-value对进行排序(只要插入key-value对时保持顺序即可),同时又可避免使用TreeMap所增加的成本。
- LinkedHashMap需要维护元素的插入顺序,因此性能略低于HashMap的性能。但因为它以链表来维护内部顺序,所以在迭代访问Map里的全部元素时将有较好的性能。
LinkedHashMap继承于HashMap,它在HashMap的基础上,通过维护一条双向链表,解决了HashMap不能随时保持遍历顺序和插入顺序一致的问题。在实现上,LinkedHashMap很多方法直接继承自HashMap,仅为维护双向链表重写了部分方法。
六、集合类(二)
1、TreeMap底层原理:TreeMap底层基于红黑树实现。映射根据其键的自然顺序进行排序,或根据创建映射时提供的Comparator进行排序,具体取决于使用的构造方法。时间复杂度logn(containsKey、get、put、remove)
2、List、Set和Map:Set无序元素不可重复、Map是具有映射关系的(key-value)集合,其所有的key是一个set集合,key无序且不能重复、List有序元素可重复。
3、ArrayList和LinkedList区别:ArrayList基于数组,LinkedList基于双向链表。ArrayList O(1)随机访问优于LinkedList O(N)。LInkedList插入删除优于ArrayList。LinkedList更占内存。
4、线程安全List:Vector、Collections.SynchronizedList、CopyOnWriteArrayList
5、CopyOnWriteArrayList:在写操作时会复制一份新的List,在新的List上完成写操作,然后再将原引用指向新的List。
6、HashSet和TreeSet:HashSet元素可以为null,TreeSet元素不能是null、HashSet不保证元素排序,TreeSet支持自然排序和定制排序、HashSet底层采用哈希表,TreeSet底层采用红黑树
7、HashSet底层:HashSet是基于HashMap实现的,默认构造函数是构建一个初始容量为16,负载因子为0.75 的HashMap。它封装了一个 HashMap 对象来存储所有的集合元素,所有放入 HashSet 中的集合元素实际上由 HashMap 的 key 来保存,而 HashMap 的 value 则存储了一个 PRESENT,它是一个静态的 Object 对象。