java基础
-
java是一种面向对象的编程语言,其特点如下:
简单易学:Java的语法设计简单,易学易用,同时Java标准库提供了丰富的API,可以帮助程序员快速实现功能。
平台无关性:Java程序可以在不同的操作系统和硬件平台上运行,这是由Java虚拟机(JVM)的存在实现的。这使得Java成为一种非常灵活的语言。
面向对象:Java是一种纯面向对象的编程语言,所有的代码都必须写在类中,可以方便地封装、继承和多态等特性。
安全性:Java程序运行在虚拟机上,严格的安全机制可以确保程序的安全性。
多线程支持:Java提供了多线程支持,程序员可以利用多线程编写高效的程序。
垃圾回收:Java内置垃圾回收机制,可以自动回收不再使用的内存空间,避免了手动管理内存的繁琐和容易出错的问题。
总的来说,Java是一种非常强大的编程语言,有着广泛的应用领域,适合开发各种类型的应用程序。 -
Final
final表示最终的,不可变的
final修饰的类无法被继承
方法不能被覆盖重写
final局部变量只能赋值一次
final的实例变量必须手动赋值,系统不负责赋默认值 -
如何将字符串反转
1.将对象封装到stringBuilder中,调用reverse方法反转
2.使用字符串数组,实现从尾部开始逐个逆序放入字符串
3.使用String的CharAt方法,使用String的CharAT方法取出字符串中的各个字符,然后插入到字符串中,调用StringBuilder的insert方法进行操作。
4.使用递归的方法,实现字符串反转 -
final、finally、finalize 有什么区别?
final可以修饰类,变量,方法,修饰的类不能被继承,修饰的变量不能重新赋值,修饰的方法不能被重写
finally用于抛异常,finally代码块内语句无论是否发生异常,都会在执行finally,常用于一些流的关闭。
finalize方法用于垃圾回收。
一般情况下不需要我们实现finalize,当对象被回收的时候需要释放一些资源,比如socket链接,在对象初始化时创建,整个生命周期内有效,那么需要实现finalize方法,关闭这个链接。
但是当调用finalize方法后,并不意味着gc会立即回收该对象,所以有可能真正调用的时候,对象又不需要回收了,然后到了真正要回收的时候,因为之前调用过一次,这次又不会调用了,产生问题。所以,不推荐使用finalize方法。 -
&和&&的区别?
&&:逻辑与运算符。当运算符左右两边的表达式都为 true,才返回 true。同时具有短路性,如果第一个表达式为 false,则直接返回 false。
&:逻辑与运算符、按位与运算符。
按位与运算符:用于二进制的计算,只有对应的两个二进位均为1时,结果位才为1 ,否则为0。
逻辑与运算符:& 在用于逻辑与时,和 && 的区别是不具有短路性。所以通常使用逻辑与运算符都会使用 &&,而 & 更多的适用于位运算。 -
两个对象的 hashCode() 相同,则 equals() 也一定为 true,对吗?
不对。
hashCode() 和 equals() 之间的关系如下:
当有 a.equals(b) == true 时,则 a.hashCode() == b.hashCode() 必然成立,
反过来,当 a.hashCode() == b.hashCode() 时,a.equals(b) 不一定为 true。
如果两个对象相同,那么他们的hashCode值一定相同 -
深拷贝和浅拷贝区别是什么?
数据分为基本数据类型和引用数据类型。
基本数据类型:数据直接存储在栈中;
引用数据类型:存储在栈中的是对象的引用地址,真实的对象数据存放在堆内存里。
浅拷贝:
对于基础数据类型:直接复制数据值;
对于引用数据类型:只是复制了对象的引用地址,新旧对象指向同一个内存地址,修改其中一个对象的值,另一个对象的值随之改变。
深拷贝:
对于基础数据类型:直接复制数据值;
对于引用数据类型:开辟新的内存空间,在新的内存空间里复制一个一模一样的对象,新老对象不共享内存,修改其中一个对象的值,不会影响另一个对象。
深拷贝相比于浅拷贝速度较慢并且花销较大。 -
构造器是否可被重写?
Constructor 不能被 override(重写),但是可以 overload(重载),所以你可以看到⼀个类中有多个构造函数的情况。在构造器中它的方法名与类名相同,在继承中会重写父类构造器,但是重写后又与父类名无法相同。 -
当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递?
值传递。Java 中只有值传递,对于对象参数,值的内容是对象的引用。 -
继承中构造方法的调用顺序
子类的构造方法总是先调用父类的构造方法,如果子类的构造方法没有明显地指明使用父类的哪个构造方法,子类就调用父类不带参数的构造方法。
而父类没有无参的构造函数,所以子类需要在自己的构造函数中显示的调用父类的构造函数。 -
堆,栈,方法区
堆区:只存放类对象,线程共享
方法区:又叫静态存储区,存放class文件和静态数据,线程共享
栈区:存放方法局部变量,基本类型变量区,执行环境上下文,操作指令区,线程不共享。 -
面向对象三大特征是什么?
封装:
实现专业的分工
减少代码耦合
可以自由修改类的内部结构
继承:
java中是不支持多继承的,接口可以,一个接口可以继承多个其他接口
也可以说子类是父类的实现,父类是子类的抽象
继承往往用于抽象的概念
例如:在数据库连接表a与表b时,可以对表a与表b来抽象它的父类,这里对获取数据库连接的方法进行抽象,这样只需关注数据库操作就行,关于数据库连接交给它的父类即可。
多态:
多态是三大特性中最重要的操作
多态是同一个行为具有多个不同表现形式或形态的能力
多态是一个接口,使用不同的实例而执行不同操作 -
静态和实例变量的区别
语法区别:静态变量前要加static关键字,实例则不用
隶属区别:实例变量隶属于某个对象的属性,而静态属于类
根本区别:静态变量在JVM加载类自己创建,不可以被垃圾回收,实例变量在实例化对象时创建
,可以被垃圾回收 -
静态块,非静态块,构造函数的执行顺序
1.静态优先
2.父类优先
3.非静态块优先于构造函数 -
接口与抽象类的不同:
概念:
抽象类:
单继承。不能被实例化,只能被继承。可以包含属性与方法,方法中可以包含具体实现。子类继承抽象类必须实现抽象类的所有方法,否则继承的类还是抽象类。
接口:
多实现。不能包含属性(成员变量),能声明方法,但方法中不包含代码实现。实现类必须实现接口的所有方法
功能:
- 抽象类:如果我们要表示一种 is-a 的关系,并且是为了解决代码复用的问题;
- 接口:如果我们要表示一种 has-a 关系,并且是为了解决解耦,提高代码的扩展性。
层次: - 抽象类:自下而上的设计思路,先有子类的代码重复,然后再抽象成上层的父类(也就是抽象类)
- 接口:自上而下的设计思路。我们在编程的时候,一般都是先设计接口,再考虑具体的实现
使用:
类与类,接口与接口是extends
接口与类是implements
抽象类中可以包含非抽象的方法,在 Java 7 之前接口中的所有方法都是抽象的,在 Java 8 之后,接口支持非抽象方法:default 方法、静态方法等。Java 9 支持私有方法、私有静态方法。
抽象类中的方法类型可以是任意修饰符,Java 8 之前接口中的方法只能是 public 类型,Java 9 支持 private 类型。
-
如何理解volatile关键字
在并发领域中,存在三大特性:原子性,有序性,可见性。volatile关键字用来就是对象属性,在并发环境下可以保证这个属性的可见性,对于加了volatile关键字的属性,在对这个属性进行修改时,会直接将CPU高级缓存中的数据写回到主内存,对这个变量的读取也会直接从主内存中读取,从而保证了可见性,底层是通过操作系统的内存屏障来实现的,由于使用了内存屏障,所以会禁止指令重排,所以同时也就保证了有序性,在很多并发场景下,如果用好volatile关键字可以很好的提高执行效率。
volatile对应的变量可能在你的程序本身不知道的情况下发生改变
比如多线程的程序,共同访问的内存当中,多个程序都可以操纵这个变量
你自己的程序,是无法判定何时这个变量会发生变化
还比如,他和一个外部设备的某个状态对应,当外部设备发生操作的时候,通过驱动程序和中断事件,系统改变了这个变量的数值,而你的程序并不知道。
对于volatile类型的变量,系统每次用到他的时候都是直接从对应的内存当中提取,**而不会利用cache当中的原有数值,**以适应它的未知何时会发生的变化,系统对这种变量的处理不会做优化——显然也是因为它的数值随时都可能变化的情况。 -
一个参数既可以是const还可以是volatile吗?解释为什么。
是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。
枚举
-
枚举允许继承类吗?
枚举不允许继承类。Jvm在生成枚举时已经继承了Enum类,由于Java语言是单继承,不支持再继承额外的类(唯一的继承名额被Jvm用了)。 -
枚举可以用等号比较吗?
枚举可以用等号比较。Jvm会为每个枚举实例对应生成一个类对象,这个类对象是用public static final修饰的,在static代码块中初始化,是一个单例。 -
枚举可以被人家继承吗?
不可以继承枚举。因为Jvm在生成枚举类时,将它声明为final。
集合
- 说说你了解的集合
- 集合从大的方向分有两个,一是Collection集合,二是Map集合。
- Collection集合下有List、Set、Queue。Map集合下有HashMap、LinkedHashMap、TreeMap、HashTable、ConcurrentHashMap。
- List集合下有ArrayList、LinkedList、Vector、CopyOnWriteArrayList。Set集合下有HashSet、LinkedHashSet、TreeSet、CopyOnWriteArraySet
- 说说对ArrayList的理解
- ArrayList是List集合的一个实现类,其底层实现是数组transient Object[] elementData;,数组的查询是直接通过索引,查询速度比较快,时间复杂度是O(1),增删的话,根据增删的位置,时间复杂度有所不同,如果是中间第i个位置,时间复杂度就是O(n-i),简单理解其时间复杂度是O(n)
- 扩容机制:当构造方法中没有指定数组的大小时,其默认初始容量是10。当超过这个默认值的时候,一定有一个扩容机制,其扩容机制是,当集合中元素的个数大于集合容量的时候,也就是add的时候集合放不下了,就会触发扩容机制,扩容后的新集合容量是旧集合容量的1.5倍,源码:int newCapacity = oldCapacity + (oldCapacity >> 1);。
- 线程问题:ArrayList是线程不安全的,在add()方法的时候,首先会检查一下数组的容量是否够用,如果够用,那么就会执行elementData[size++] = e;方法,该语句执行了两大步,第一大步是,将e放到elementData缓冲区,第二大步是,将size的大小进行加1操作,也就是说,这个操作并非原子性操作。当在并发的情况时,就会出现问题。
- 说说对LinkedList的理解
- LinkedList也是List的一个实现类,其底层是双向链表,其内部有一个next指针指向下一个节点,一个prev指针,指向上一个节点,由于是链表的数据结构,所以在查询的时候相对就比较慢了,时间复杂度是O(n),因为当我们需要查询某个元素的时候,需要从第一个节点开始遍历,直到查询结束。而他的增删就比较快了,如果增加一个N节点,直接将后一个节点的prev指向N节点,N节点的next指向后一个节点,前一个节点的next指向N节点,N节点的prev指针指向前一个节点即可,时间复杂度为O(1),空间复杂度一般比ArrayList大,因为每个节点都要存储两个指针。
- 线程问题:LinkedList也是线程不安全的,其添加元素的操作,通过linkLast方法在尾部进行添加的,添加完之后,并把size的大小加1。其他的不说,单单一个size++就不是原子性了。简单的a加1操作会执行三步:1:把a的值加载到内存、2:将内存中的值,存储到变量中、3:然后进行加1操作。
- 说说对Vector的理解
Vector也是List的一个实现类,其底层也是一个数组protected Object[] elementData;,底层ArrayList差不多,也就是加了synchronized的ArrayList,线程是安全的,效率没有ArrayList高,一般不建议使用。 - 如果不使用Vector来解决ArrayList的线程安全问题,还有其他的解决方案吗?
- 既然不建议使用Vector,还有一个包java.util.concurrent(JUC)包,它下面有一个类CopyOnWriteArrayList也是线程安全的。CopyOnWriteArrayList也是List的一个实现类。
- add方法用Lock锁来解决并发问题,其中在进行添加数据的时候,用了copyOf方法,也就是复制了一份,然后再set进去。
- CopyOnWriteArrayList底层也是用的数组,但是它的数组是用volatile修饰了,主要是保证了数据的可见性。get操作时,并没有加锁,因为volatile保证了数据的可见性,当数据被修改的时候,读操作能立刻知道。
- 说说List和Set的区别
- List的存储顺序是按照存入的顺序来的,而Set是根据哈希值来的
- List可以存储相同的元素,Set不可以存储相同的元素
- 说说对HashSet的理解
- HashSet是Set集合的一个实现类,其底层实现是HashMap的key,初始化容量是16,负载因子是0.75,扩容机制,是变为原来的2倍。
- HashSet存储元素的顺序并不是按照存入时的顺序(和List不同)而是按照哈希值来存的所以取数据也是按照哈希值取得。元素的哈希值是通过元素的hashcode方法来获取的, HashSet首先判断两个元素的哈希值,如果哈希值一样,接着会比较equals方法 如果 equals结果为true ,HashSet就视为同一个元素。如果equals 为false就不是同一个元素。 哈希值相同equals为false的元素是怎么存储呢,就是在同样的哈希值下顺延(可以认为哈希值相同的元素放在一个哈希桶中)。也就是哈希一样的存一列。
- 说说对LinkedHashSet的理解
- LinkedHashSet是对在HashSet的基础上维护了一个双向链表,使得LinkedHashSet存取有序。
- 说说对TreeSet的理解
- TreeSet()是使用二叉树的原理对新add()的对象按照指定的顺序排序(升序、降序),每增加一个对象都会进行排序,将对象插入的二叉树指定的位置。
- 如果想要对HashSet进行线程安全处理,应该怎么办?
- 可以通过CopyOnWriteArraySet来实现。
- queue是什么呢
- queue是一个队列,先进先出(FIFO)的数据结构
- 聊聊对HashMap的理解(重要)
- 底层是JDK1.7是通过数组+链表JDK1.8是通过数组+链表+红黑树组成。所有的数据都是通过一个Node节点进行封装,其中Node节点中封装了hash值,key,value,和next指针。hash是通过key计算出的hashCode值进行对数组容量减一求余得到的(官方的求余方式是通过&运算进行的)。不同的key计算出来的hash值可能相同,解决冲突是通过拉链法(链表和红黑树)进行处理。正是因为这种存储形势,所以HashMap的存取顺序是无序的。
- 懒加载机制,在put值的时候会判断数组是否为空,如果是就初始化数组,而不是new的时候就初始化。
- HashMap是Map的一个实现类,其默认初始化容量大小是16。扩容机制是根据扩容因子来扩容的,当容量的使用量达到总容量的0.75时,就会触发扩容,举例说就是,当总容量是16时,使用量达到12,就会触发扩容机制。
- 当我们put一个值的时候,通过key来计算出hash值,计算出来的hash值做为数组的索引,Node节点中封装了hash值,key,value和next。当链表的长度小于8的时候,处理冲突的方式是链表。大于等于8的时候,就会触发红黑树方式存储。
- 当元素个数小于等于6的时候,会触发红黑树转化为链表的形式,为什么不是小于等于7,是因为给一个过度,也就是防止添加一个刚好为8,删除一个刚好为7,这样来回转化。
- 说说对LinkedHashMap的理解
- LinkedHashMap解决了HashMap不能保证存取顺序的问题。内部增加了一个链表用于维护元素存取顺序。
- 说说对TreeMap的理解
- TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序。
- 如果想要保证HashMap的线程安全,应该怎么办?
- 可以通过HashTable,该类的出现主要是解决了HashMap的线程安全问题,直接用了synchronized锁,所以效率上不高,不建议使用(发现JDK1.0的线程问题,解决都很暴力)。
- ConcurrentHashMap是java.util.concurrent包下的,并发包下的。他就是对HashMap进行了一个扩展,也就是解决了他的线程安全问题。ConcurrentHashMap用了大量的CAS来进行优化。
- 什么是CAS呢?
- CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术。简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替换当前变量的值。
- Collection 和 Collections 有什么区别?
- Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List与Set。
- Collections则是集合类的一个工具类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。
- ArrayList和LinkedList 的区别是什么?
- ArrayList底层的数据结构是数组,支持随机访问,而 LinkedList 的底层数据结构是双向循环链表,不支持随机访问。使用下标访问一个元素,ArrayList 的时间复杂度是 O(1),而 LinkedList 是 O(n)。
- ArrayList和Vector 的区别是什么?
- Vector使用了synchronized来实现线程同步,是线程安全的,而ArrayList是非线程安全的。
- Vector扩容每次会增加1倍,而ArrayList只会增加0.5倍。
- HashMap和HashTable的区别?
- HashMap允许空键值,HashTable不允许。
- HashMap线程不安全(效率高),HashTable线程安全。
- 总结一下HashMap和Hashtable的区别:
- HashMap是Hashtable的轻量级实现,它们都实现了Map接口,主要区别在于HashMap允许空(null)键值(key),而Hashtable不允许。
- HashMap没有contains方法,而是containsValue和containsKey。
- Hashtable的方法是线程安全的,而HashMap不是线程安全的。
- HashMap使用Iterator,Hashtable使用Enumeration。
- HashMap和Hashtable采用的hash/rehash算法都几乎一样,所以性能不会有很大的差异。
- 在Hashtable中,hash数组默认大小是11,增加的方式是old*2+1。在Has和Map中,hash数组的默认大小是16,而且一定是2的倍数。
- hash值的使用不同,Hashtable直接使用对象的hashCode。
-
集合(Set)和数组(Array)的区别
数据类型不同:数组是由一组相同数据类型的元素组成,而集合是由一组唯一的元素组成,这些元素可以是不同类型的。
存储方式不同:数组是使用连续的内存地址存储,所以访问元素时可以通过下标实现快速访问。而集合存储元素的方式与具体实现有关,一般是使用哈希表或二叉树的方式,所以访问元素的效率与数据量成正比。
增删操作效率不同:数组的插入和删除操作不如集合高效,因为当需要插入或删除一个元素时,数组需要移动其他元素来填补空隙,而集合通常只需要修改相关指针或哈希值即可。
元素唯一性不同:集合中的元素是唯一的,不能重复,而数组中可以存在相同的元素。
功能不同:集合一般提供集合运算操作(求并集、交集等),查找、插入、删除元素等操作,而数组主要用于存储一组相同类型的元素,提供访问、插入、删除等操作。 -
HashMap
Java中的HashMap是一种基于哈希表的数据结构,它实现了Map接口,提供了键(Key)和值(Value) 的映射关系。在HashMap中,键和值都可以为null。
在HashMap内部,它的核心是一个Entry数组,每个Entry对应一个键值对。每个Entry包含四个域,分别是: -
int类型的hash值,用于快速定位到这个键值对所在的位置。
-
K类型的Key,用来唯一标识这个键值对。
-
V类型的Value,表示键所对应的值。
-
一个指向下一个Entry的引用,因为在HashMap中同一个桶可能会存放多个元素,这些元素都存在一个链表中,使用next指针指向下一个Entry。
在使用HashMap时,当我们插入一个键值对时,首先需要根据Key的hash值计算得到这个键值对在数组中的位置,然后在这个位置上查找是否已经存在同样的Key,若存在则用新的Value代替旧的Value,若不存在则新创建一个Entry并插入到对应的数组位置。在查询一个Value时,也是先计算Key的hash值,然后根据hash值定位到相应的位置,再依次遍历对应链表上的每个Entry,直到找到对应的Key为止。
HashMap在插入、查询等操作时具备较高的性能,但当哈希算法设计不够合理或者哈希冲突发生概率较高时,性能也会受到影响。同时,在多线程环境下,需要使用ConcurrentHashMap等线程安全的Map实现来避免并发导致的问题。 -
HashMap的put方法:
-
对Key进行Hash运算,得到它的HashCode。如果Key为null,则HashCode为0。
-
将HashCode通过Hash算法计算得到它在Entry数组中的位置(注意:这个位置可能已经有其它的元素了)。
-
遍历相同HashCode的元素,查看它们是否与当前要添加的Key相等。
-
如果想等,则覆盖新的Value,否则将新元素添加到链表中的头部。
-
如果链表长度达到8,需要将链表转换为红黑树,以提高查找效率。
-
如果HashMap中的元素个数size超过了负载因子(load factor)阈值(threshold),则需要进行扩容,来保证HashMap的效率。
整个流程中,最耗时的操作其实是第3步——查找相同HashCode的元素,这个步骤的时间复杂度在最坏情况下甚至会达到O(N)(N为链表长度),所以保证hashCode的分布均匀性非常重要,只有hashCode的分布均匀,才能够保证相同HashCode的元素数量足够小。因此在设计hashCode时需要考虑到这一点。 -
HashMap的扩容机制
1.7版本
先生成新数组
遍历老数组中的每个位置上的链表上的元素
获取每个元素的key,并基于新数组长度,计算出每个元素在新数组中的下标
将元素添加到新数组中去
所有元素转移完了之后,将新数组赋值给HashMap对象的table属性
1.8版本
先生成新数组
遍历老数组中的每个位置上的链表或红黑树
如果是链表,则直接将链表中的每个元素重新计算下标,并添加到新数组中去
如果是红黑树,则先遍历红黑树,先计算出红黑树中每个元素对应在新数组中的下标位置
- 统计每个下标位置的元素个数
- 如果该位置下的元素个数超过了8,则生成一个新的红黑树,并将根节点的添加到新数组的对应位置
- 如果该位置下的元素个数没有超过8,则生成一个链表,并将链表的头节点添加到新数组的对应位置
所有元素转移完了之后,将数组赋值给HashMap对象的table属性。
- ConcurrentHashMap的扩容机制
1.7版本
ConcurrentHashMap是基于segment分段实现的
每个segment相对于一个小型的HashMap
每个segment内部会进行扩容,和HashMap的扩容逻辑类似
先生成新的数组,然后转移元素到新数组中
扩容的判断也是每个segment内部单独判断的,判断是否超过阈值
1.8版本
ConcurrentHashMap是不在基于segment实现的
当某个线程进行put时,如果发现ConcurrentHashMap正在进行扩容那么该线程一起进行扩容
如果某个线程put时,发现没有正在进行扩容,则将key-value添加到ConcurrentHashMap中,然后判断是否超过阈值,超过了则进行扩容
ConcurrentHashMap是支持多个线程同时扩容的
扩容之前也先生成一个新的数组
在转移元素时,先将原数组分组,将每组分给不同的线程来进行元素转移工作
- map:HashMap、TreeMap和HashTable
线程安全
HshaMap线程不安全
TreeMap线程不安全
HashTable线程安全
空值
HashMap一个null key,多个null value
TreeMap不能null key,多个null value
HashTable都不能有null
继承和接口
HashMap继承AbstractMap,实现接口Map
TreeMap继承AbstractMap,实现接口NavigableMap(SortMap的一种)
HashTable继承Dictionary,实现接口Map
顺序
HashMap中key是无序的
TreeMap是有序的
HashTable是无序的
构造函数
HashMap有调优初始容量和负载因子
TreeMap没有
HashTable有
数据结构
HashMap是链表+数组+红黑树
TreeMap是红黑树
HashTable是链表+数组