汇总:华为通用软件面试基础问题(java)(最新)
1、Collection接口下有哪些子类?
1.1 集合和数组的区别
1.2 Collection和Map
1.3 Collection接口
1.4 集合的遍历方法
用迭代器迭代
Iterator it = list.iterator();
while(it.hasNext()) {
System.ou.println(it.next);
}
1.5 List接口的实现类
(1)ArrayList:底层数据结构是数组,查询快,增删慢,线程不安全,效率高,可以存储重复元素
(2)LinkedList 底层数据结构是链表,查询慢,增删快,线程不安全,效率高,可以存储重复元素
(3)Vector:底层数据结构是数组,查询快,增删慢,线程安全,效率低,可以存储重复元素
1.6 Set集合
(1)HashSet : 底层数据结构采用哈希表实现,元素无序且唯一,线程不安全,效率高,可以存储null元素,元素的唯一性是靠所存储元素类型是否重写hashCode()和equals()方法来保证的,如果没有重写这两个方法,则无法保证元素的唯一性。
a、实现唯一性:
存储元素首先会使用hash()算法函数生成一个int类型hashCode散列值,然后已经的所存储的元素的hashCode值比较,如果hashCode不相等,则所存储的两个对象一定不相等,此时存储当前的新的hashCode值处的元素对象;如果hashCode相等,存储元素的对象还是不一定相等,此时会调用equals()方法判断两个对象的内容是否相等,如果内容相等,那么就是同一个对象,无需存储;如果比较的内容不相等,那么就是不同的对象,就该存储了,此时就要采用哈希的解决地址冲突算法,在当前hashCode值处类似一个新的链表, 在同一个hashCode值的后面存储存储不同的对象,这样就保证了元素的唯一性。
b、实现不重复
HashSet也一样他是使用了一种标识来确定元素的不重复,HashSet用一种算法来保证HashSet中的元素是不重复的, HashSet采用哈希算法,底层用数组存储数据。默认初始化容量16,加载因子0.75。
Object类中的hashCode()的方法是所有子类都会继承这个方法,这个方法会用Hash算法算出一个Hash(哈希)码值返回,HashSet会用Hash码值去和数组长度取模, 模(这个模就是对象要存放在数组中的位置)相同时才会判断数组中的元素和要加入的对象的内容是否相同,如果不同才会添加进去。
(2)、LinkedHashSet底层数据结构采用链表和哈希表共同实现,链表保证了元素的顺序与存储顺序一致,哈希表保证了元素的唯一性。线程不安全,效率高。
(3)、TreeSet底层数据结构采用二叉树来实现,元素唯一且已经排好序;唯一性同样需要重写hashCode和equals()方法,二叉树结构保证了元素的有序性。根据构造方法不同,分为自然排序(无参构造)和比较器排序(有参构造)。
自然排序要求元素必须实现Compareable接口,并重写里面的compareTo()方法,元素通过比较返回的int值来判断排序序列,返回0说明两个对象相同,不需要存储;
比较器排需要在TreeSet初始化是时候传入一个实现Comparator接口的比较器对象,或者采用匿名内部类的方式new一个Comparator对象,重写里面的compare()方法;
**
比较:
**
1、TreeSet 是二叉树(红黑树的树据结构)实现的,Treeset中的数据是自动排好序的,不允许放入null值;
2、HashSet 是哈希表实现的,HashSet中的数据是无序的,可以放入null,但只能放入一个null,两者中的值都不能重复,就如数据库中唯一约束 ;
3、HashSet要求放入的对象必须实现HashCode()方法,放入的对象,是以hashcode码作为标识的,而具有相同内容的String对象,hashcode是一样,所以放入的内容不能重复。但是同一个类的对象可以放入不同的实例;
适用场景分析:HashSet是基于Hash算法实现的,其性能通常都优于TreeSet。为快速查找而设计的Set,我们通常都应该使用HashSet,在我们需要排序的功能时,我们才使用TreeSet。
1.7 List和Set的比较
(1)、List,Set都是继承自Collection接口;
(2)、List特点:元素有放入顺序,元素可重复 ;Set特点:元素无放入顺序,元素不可重复,重复元素会覆盖掉;
(3)、Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变。
https://www.cnblogs.com/dongtian-blogs/p/10861138.html
2 排序算法有哪些?冒泡算法的时间复杂度和空间复杂度
排序算法分为两种:比较算法、非比较算法
1、比较算法时间复杂度O(nlogn) ~ O(n^2)
冒泡排序、选择排序、插入排序、归并排序、堆排序、快速排序
2、非比较排序O(n)
计数排序、基数排序、桶排序
特点:排序算法的稳定性:如果Ai = Aj,排序前Ai在Aj之前,排序后Ai还在Aj之前,则称这种排序算法是稳定的
排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,前一个键排序的结果可以为后一个键排序所用
具体算法以及代码请参考: https://editor.csdn.net/md/?articleId=105536263
时间复杂度
这个时间复杂度还是很好计算的:外循环和内循环以及判断和交换元素的时间开销;
最优的情况也就是开始就已经排序好序了,那么就可以不用交换元素了,则时间花销为:[ n(n-1) ] / 2;所以最优的情况时间复杂度为:O( n^2 );
最差的情况也就是开始的时候元素是逆序的,那么每一次排序都要交换两个元素,则时间花销为:[ 3n(n-1) ] / 2;(其中比上面最优的情况所花的时间就是在于交换元素的三个步骤);所以最差的情况下时间复杂度为:O( n^2 );
综上所述:
最优的时间复杂度为:O( n^2 ) ;有的说 O(n),下面会分析这种情况;
最差的时间复杂度为:O( n^2 );
平均的时间复杂度为:O( n^2 );
空间复杂度
空间复杂度就是在交换元素时那个临时变量所占的内存空间;
最优的空间复杂度就是开始元素顺序已经排好了,则空间复杂度为:0;
最差的空间复杂度就是开始元素逆序排序了,则空间复杂度为:O(n);
平均的空间复杂度为:O(1);
1、时间复杂度:
(1)时间频度 一个算法执行所耗费的时间,从理论上是不能算出来的,必须上机运行测试才能知道。但我们不可能也没有必要对每个算法都上机测试,只需知道哪个算法花费的时间多,哪个算法花费的时间少就可以了。并且一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)。
(2)时间复杂度 在刚才提到的时间频度中,n称为问题的规模,当n不断变化时,时间频度T(n)也会不断变化。但有时我们想知道它变化时呈现什么规律。为此,我们引入时间复杂度概念。 一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称O(f(n)) 为算法的渐进时间复杂度,简称时间复杂度。
说人话:举例来
for (i=1; i<=n; i++)
x++;
for (i=1; i<=n; i++)
for (j=1; j<=n; j++)
x++;
第一个for循环的时间复杂度为Ο(n),第二个for循环的时间复杂度为Ο(n2),则整个算法的时间复杂度为Ο(n+n2)=Ο(n2)。
再来一个例子:
i=1; ①
while (i<=n)
i=i*2; ②
解: 语句1的频度是1,
设语句2的频度是f(n), 则:2^f(n)<=n; f(n)<=log2n
取最大值f(n)=log2n,
T(n)=O(log2n )
这下应该明白时间复杂度怎么求了吧。。。不会求的直接背也行,记得是一个平均值
2、空间复杂度
类似于时间复杂度的讨论,一个算法的空间复杂度(Space Complexity)S(n)定义为该算法所耗费的存储空间,它也是问题规模n的函数。渐近空间复杂度也常常简称为空间复杂度。
存储算法本身所占用的存储空间与算法书写的长短成正比,要压缩这方面的存储空间,就必须编写出较短的算法。
算法在运行过程中临时占用的存储空间随算法的不同而异,有的算法只需要占用少量的临时工作单元,而且不随问题规模的大小而改变,我们称这种算法是“就地"进行的,是节省存储的算法,如这一节介绍过的几个算法都是如此;有的算法需要占用的临时工作单元数与解决问题的规模n有关,它随着n的增大而增大,当n较大时,将占用较多的存储单元,例如将在第九章介绍的快速排序和归并排序算法就属于这种情况。
说人话:代码长短、代码运行时占用的临时存储空间
怎么求呢?
当一个算法的空间复杂度为一个常量,即不随被处理数据量n的大小而改变时,可表示为O(1);
当一个算法的空间复杂度与以2为底的n的对数成正比时,可表示为0(10g2n);
当一个算法的空I司复杂度与n成线性比例关系时,可表示为0(n).
若形参为数组,则只需要为它分配一个存储由实参传送来的一个地址指针的空间,即一个机器字长空间;
若形参为引用方式,则也只需要为其分配存储一个地址的空间,用它来存储对应实参变量的地址,以便由系统自动引用实参变量。
问题在于你怎么判断空间复杂度为什么呢?
举例
int[] m = new int[n]
for(i=1; i<=n; ++i)
{
j = i;
j++;
}
这段代码中,第一行new了一个数组出来,这个数据占用的大小为n,这段代码的2-6行,虽然有循环,但没有再分配新的空间,因此,这段代码的空间复杂度主要看第一行即可,即 S(n) = O(n)
转载的:https://www.cnblogs.com/jsjwk/p/9993802.html
那就是说和分配空间相关了?应该是分配的空间会随N变化满足上面第3条的要求,所以 S(n) = O(n) 这个解释应该是更好一些
3 java数据类型有哪些?
基本数据类型
封装类型
引用数据类型: 数组、类、接口
4 java的异常分类
Java语言按照错误严重性,从throwale根类衍生出Error和Exception两大派系
**Error(错误):**程序在执行过程中所遇到的硬件或操作系统的错误。错误对程序而言是致命的,将导致程序无法运行。
常见的错误有内存溢出,jvm虚拟机自身的非正常运行,calss文件没有主方法。
程序本身是不能处理错误的,只能依靠外界干预。
Error是系统内部的错误,由jvm抛出,交给系统来处理。
EXCEPTION(异常):是程序正常运行中,可以预料的意外情况。
比如数据库连接中断,空指针,数组下标越界。
异常出现可以导致程序非正常终止,也可以预先检测,被捕获处理掉,使程序继续运行。
编译异常(可检测)和运行时异常(不可检测)
**编译时异常:**又叫可检查异常,通常时由语法错和环境因素(外部资源)造成的异常。比如输入输出异常IOException,数据库操作SQLException。其特点是,Java语言强制要求捕获和处理所有非运行时异常。通过行为规范,强化程序的健壮性和安全性。
**运行时异常:**又叫不检查异常RuntimeException,这些异常一般是由程序逻辑错误引起的,即语义错。比如算术异常,空指针异常NullPointerException,下标越界IndexOutOfBoundsException。运行时异常应该在程序测试期间被暴露出来,由程序员去调试,而避免捕获。
java语言处理运行时错误有三种方式:
一是程序不能处理的错误,
二是程序应该避免而可以不去捕获的运行时异常,
三是必须捕获的非运行时异常。
5 OutOfMemoryError是哪种异常?
内存溢出异常
操作系统为每个进程分配的内存是具有一定限制性。譬如,32位的操作系统限制为2GB。而当检查出哪个区域出现OutOfMemoryError异常时,需要平衡该进程的某个区域内存大小来解决另一个区域出现内存溢出的情况。例如,在高并发情况下,不断地创建线程可能会使线程私有的虚拟机栈出现内存溢出情况,在不能减少线程数或者更换更大位数的操作系统时,就只能通过减少最大堆和减少栈容量的方式来解决虚拟机栈导致的内存溢出。所以,无论在处理异常或者是实验,我们都要学会调整设置虚拟机启动参数
以下为了解内容
1、Java堆溢出原因及其解决方案
Java堆用于存储对象实例和数组的,只要不断地创建对象或者数组,并且保证GC Roots到对象之间有可达的路径来避免垃圾回收机制清除这些对象,那么在对象数量达到最大堆的容量限制后就会产生内存溢出异常。
要了解Java堆内存可能溢出原因,首先我们要知道当生成新对象时,向Java堆申请内存的过程:
① JVM先尝试在Eden区(Young区包括1个Eden和2个survivor)分配新建对象所需要的内存;
② 如果内存大小足够,则申请结束。反之,执行下一步;
③ JVM启动新生代GC,试图将Eden区中不活跃的对象释放掉,释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区;
④ Survivor区被用来作为Eden及old的中间交换区域,当old区空间足够时,Survivor区的对象会被移到old区,否则会被保留在Survivor区;
⑤ 当old区空间不够时,JVM会在old区进行GC;
⑥ old区被清理后,若old区仍无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新生对象创建内存区域,则会出现内存溢出异常(OutOfMemmory)。
原文链接:https://blog.csdn.net/qq_41285600/article/details/82798920
解决方案:
Java堆出现内存溢出异常有两种解决方案。一种是基于内存调整来改变堆区内存大小以便能够存储更多的对象,但堆内存受到物理内存的限制,当出现无法再扩展堆内存的情况时,就采用第二种方式,从代码上检查是否存在某些对象的生命周期过长、持有状态时间过长的情况,尝试减少程序在运行期的内存消耗。
2、虚拟机栈和本地方法栈溢出
虚拟机栈和本地方法栈的内存分布在不同的虚拟机上是有差别的。例如,在HotSpot上是将虚拟机栈和本地方法栈合二为一的。虽然有-Xoss参数用于设置本地方法栈,但实际上是无效的,栈容量只由-Xss参数设定。关于虚拟机栈和本地方法栈,在Java虚拟机规范中描述了两种异常:①如果线程请求的栈深度大于虚拟机所允许的最大栈深度,将抛出*Error异常。②如果虚拟机在扩展栈时无法申请到足够的空间,则抛出OutOfMemoryError异常。
导致这两种异常的原因是有一定区别的。虚拟机栈是线程私有的,它存储的是存放着局部变量表、操作数栈、动态链接以及方法出口等信息的栈帧,当前线程每执行一个方法时,实际上都对应着一个栈帧在虚拟机栈中出栈到入栈的过程,而当这个线程执行方法较多时,创建的栈帧也会随之越来越多,而虚拟机栈内存是受到了JVM总体内存的一个分布限制,一旦栈帧较多时,可能就会出现栈满溢出异常,也就是*Error异常。而出现OutOfMemoryError异常的情况是相对于多线程环境下而言的,一旦线程数量过多,并且都没有即使回收,从而会不断地申请内存给虚拟机栈,从而导致在扩展栈时无法申请到足够的空间,出现OutOfMemoryError异常。当然,OutOfMemoryError异常也可能出现在单钱程的情况下。当为一个线程设置虚拟机栈内存大小与其它区内存之和大于JVM所允许的最大内存,就可能出现OutOfMemoryError异常。
3、方法区和运行时常量池溢出
在jdk1.7之前,运行时常量池属于方法区的一部分,也就是运行时常量池处于永久代。而方法区出现的内存溢出异常经常是由运行时常量池内存溢出异常所导致的。但在jdk1.7时,为了消除永久代,常量池位置发生了一定的变化,由原来的方法区转移到了堆区,但永久代实际上还是存在的,因为内加载机制还在永久代。我们也知道,堆区是GC管理的主要区域,而常量池存储了该类的所有常量,如果使其处于永久代不进行及时的回收,导致内存溢出可能会是经常的。放入堆区个人理解就是为了方便及时合理回收运行时常量池。在jdk1.8之后,完全消除永久代这一个区域,jvm将方法区放入了一个与堆不相连的区域,该区域称为元空间(元空间介绍引用:https://www.cnblogs.com/duanxz/p/3520829.html)。
4、本机直接内存溢出
本机直接内存容量可以通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆最大值(-Xmx制定)一样。NIO提供了一个不经过JVM直接访问本机物理内存的类——DirectMemory。DircetMemory继承与ByteBuffer,但和普通的ByteBuffer不同,普通的ByteBuffer在堆上进行内存分配,其最大的内存受到堆内存的限制。而DircetMemory是向本机物理内存申请内存分配,所以其大小只受物理内存的限制。由于直接内存是介于堆区和操作系统之间的一个Buffer,所以读写操作比普通的Buffer要快,同样也造成创建和销毁较普通Buffer慢的特点。以下代码清单中直接越过DircetByteBuffer(存储在Java堆上的操作直接内存的一个对象引用)类,直接通过反射获取unsafe实例进行内存分配。因为虽然使用了DircetByteBuffer分配内存也会抛出内存溢出异常,但它抛出异常时并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配,于是手动抛出异常,真正申请分配内存的方法是unsafe.allocateMemory()。
6、Hashmap、Hashtable、ConcurrentHashmap的原理和区别
HashTable
底层数组+链表实现,无论key还是value都不能为null,线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMap做了相关优化
初始size为11,扩容:newsize = olesize*2+1
计算index的方法:index = (hash & 0x7FFFFFFF) % tab.length
HashMap
底层数组+链表实现,可以存储null键和null值,线程不安全
初始size为16,扩容:newsize = oldsize*2,size一定为2的n次幂
扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入
插入元素后才判断该不该扩容,有可能无效扩容(插入后如果扩容,如果没有再次插入,就会产生无效扩容)
当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀
计算index方法:index = hash & (tab.length – 1)
ConcurrentHashMap
底层采用分段的数组+链表实现,线程安全
通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)
Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术
有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁
扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容
锁分段技术:
首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
ConcurrentHashMap提供了与Hashtable和SynchronizedMap不同的锁机制。Hashtable中采用的锁机制是一次锁住整个hash表,从而在同一时刻只能由一个线程对其进行操作;而ConcurrentHashMap中则是一次锁住一个桶。
ConcurrentHashMap默认将hash表分为16个桶,诸如get、put、remove等常用操作只锁住当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有16个写线程执行,并发性能的提升是显而易见的。
6、java的锁机制分类
6.1自旋锁
自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
6.2 偏向锁
偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。
6.3 轻量级锁
轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
6.4 重量级锁Synchronized
当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen(jdk1.8则是metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程
7、Synchronized有多少种用法?锁方法和锁代码块哪种比较好?听说过锁类和锁实例么?
7.1、同步普通方法(锁实例对象)
这个也是我们用得最多的,只要涉及线程安全,上来就给方法来个同步锁。这种方法使用虽然最简单,但是只能作用在单例上面,如果不是单例,同步方法锁将失效。
/**
* 用在普通方法
*/
private synchronized void synchronizedMethod() {
System.out.println("synchronizedMethod");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
此时,同一个实例只有一个线程能获取锁进入这个方法。
7.2、同步静态方法(锁类本身)
同步静态方法,不管你有多少个类实例,同时只有一个线程能获取锁进入这个方法。
/**
* 用在静态方法
*/
private synchronized static void synchronizedStaticMethod() {
System.out.println("synchronizedStaticMethod");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
同步静态方法是类级别的锁,一旦任何一个线程进入这个方法,其他所有线程将无法访问这个类的任何同步类锁的方法。
7.3、同步类(锁类本身)
下面提供了两种同步类的方法,锁住效果和同步静态方法一样,都是类级别的锁,同时只有一个线程能访问带有同步类锁的方法。
/**
* 用在类
*/
private void synchronizedClass() {
synchronized (TestSynchronized.class) {
System.out.println("synchronizedClass");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 用在类
*/
private void synchronizedGetClass() {
synchronized (this.getClass()) {
System.out.println("synchronizedGetClass");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
这里的两种用法是同步块的用法,这里表示只有获取到这个类锁才能进入这个代码块。
7.4、同步this实例(锁实例对象)
这也是同步块的用法,表示锁住整个当前对象实例,只有获取到这个实例的锁才能进入这个方法。
/**
* 用在this
*/
private void synchronizedThis() {
synchronized (this) {
System.out.println("synchronizedThis");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
用法和同步普通方法锁一样,都是锁住整个当前实例。
7.5、同步对象实例
这也是同步块的用法,和上面的锁住当前实例一样,这里表示锁住整个 LOCK 对象实例,只有获取到这个 LOCK 实例的锁才能进入这个方法。
/**
* 用在对象
*/
private void synchronizedInstance() {
synchronized (LOCK) {
System.out.println("synchronizedInstance");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
1.当加锁的方法是实例方法时,同一个对象在多线程情况下不能同时访问加锁的实例方法,换句话意思是,同一个实例内的加锁实例方法是相互约束的。当然,对于不同的实例对象,因为在不同的域,就没有这种约束。
2.当加锁的方法是类方法时,属于该类的对象在多线程情况下不能同时访问加锁的静态方法,也就是加锁的静态方法在该类下都是相互约束的,作用域是整个类。
3.线程可同时获得实例方法和类方法的锁,这点很关键。看测试3结果就能发现,类锁和对象锁控制着两个不同区域,互不约束。
4当加锁的代码块时,同一个实例对象在多线程情况下不能同时访问加锁的代码块,但是方法的未加锁段是可以同时访问的
8、java的内存分区
Java程序是交由JVM执行的,所以Java内存区域划分的时候事实上是指JVM区域划分
如图所示,首先Java源代码文件(.java后缀)会被Java编译器编译为字节码文件(.class后缀),然后由JVM中的类加载器加载各个类的字节码文件,加载完毕之后,交由JVM执行引擎执行。在整个程序执行过程中,JVM会用一段空间来存储程序执行期间需要用到的数据和相关信息,这段空间一般被称作Runtime Data Area(运行时数据区)也就是我们常常说的JVM内存。因此,在Java中我们常常说到的内存管理就是针对这段空间进行管理(如何分配和回收内存空间)。
运行时数据区的每部分到底存储了那些数据?
1、程序计数器
程序计数器(Program Counter Regist)也有称作为PC寄存器,在汇编语言中,程序计数器是指CUP中的寄存器就,它保存的是程序当前执行的指令地址(也可以说是下一条指令的所在存储单元地址),当CUP需要指令时,需要从程序计数器中得到当前 执行的指令所在存储单元地址,然后根据得到的地址获取到指令,在得到指令后,程序计数器便会自动加1或者根据转移指针得到下一条指令的地址,如此循环,直至执行完所有指令。
2、Java栈
Java栈也称作是虚拟机栈(Java Vitual Machine Stack),也就是我们常常所说的栈,跟c语言的数据段中的栈类似。事实上,Java栈是Java方法执行的内存模型。
Java栈中存放的是一个个栈帧,每个栈帧对应着一个被调用的方法,在栈帧中包括局部变量表(Local Variable)、操作数栈(Operaand Stack)、指向当前方法所属的类的运行时常量池的引用(Reference to runtime constant tool)、方法返回地址(Return Address)和一些额外的附加信息。当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。因此可知,线程当前执行的方法所对应的栈帧必定位于Java栈的顶部。
3、本地方法栈
本地方法栈与Java栈的作用和原理相似,区别只不过是Java栈是为执行Java方法服务的,而本地方法栈则是执行本地方法(Native Method)服务的,在JVM规范中,并没有对本地方法栈的具体实现方法以及数据结构做强制规定,虚拟机可以*实现它,在HOTSpot虚拟机中直接把本地方法栈和Java栈合二为一。
4、堆
在c语言中,堆这部分空间是唯一一个程序员管理的内存区域,程序员可以通过malloc函数和free函数在堆上申请和释放空间
Java中的堆是用来存储对象本身以及数组(当然,数组引用是放在Java栈中的)。只不过和c语言不通,在Java中,程序员基本不关心空间释放的问题,Java的垃圾回收机制会自动进行处理,因此这部分空间也是Java垃圾收集器管理的主要区域。另外堆是被所有线程池共享的,在JVM中只有一个堆。
5、方法区
方法区在JVM中也是一个非常重要的区域,它与堆一样,是被线程池共享的区域。在方法区中,存储每个类的信息(包括类的名称、方法信息、字段信息)静态变量、常量以及编译器变异后的的代码等。
在class文件中除了类的字段、方法、接口等描述信息外,还有一项是常量池,用来存储编译期间生成的字面量和符号引用。
在方法区中有一个非常重要的部分就是运行时常量池,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的常量池就被创建出来。当然并非class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如string的intern方法。
9、利用jdbc进行查询的步骤
七个步骤:
第一步:加载JDBC驱动程序
第二步:创建连接
第三步:写sql
第四步:得到statement对象
第五步:执行sql 得到结果集
第六步:处理结果集
第七步:关闭资源
1、关闭记录集
2、关闭声明
3、关闭连接对象
10、PreparedStatement和Statement区别
1、 执行静态sql语句时,通常通过Statement实例实现。
2、 执行动态sql语句时,通常通过PreparedStatement实例实现。
3、 执行数据库存储过程,通常通过CallableStatement实例实现。
联系:
1.PreparedStatement和Statement都是用来执行SQL查询语句的API之一
2.PreparedStatement接口继承了Statement接口
区别:
Statement不对sql语句作处理,直接交给数据库;而PreparedStatement支持预编译,会将编译好的sql语句放在数据库端,相当于缓存。对于多次重复执行的sql语句,使用PreparedStatement可以使得代码的执行效率更高。
Statement的sql语句使用字符串拼接的方式,容易导致出错,且存在sql注入的风险;PreparedStatement使用“?”占位符提升代码的可读性和可维护性,并且这种绑定参数的方式,可以有效的防止sql注入。
11、基本数据类型和包装数据类的区别?为什么使用包装类?
1、包装类是对象,拥有方法和字段,对象的调用都是通过引用对象的地址;基本类型不是
2、包装类型是引用的传递;基本类型是值的传递
3、声明方式不同:
基本数据类型不需要new关键字;
包装类型需要new在堆内存中进行new来分配内存空间
4、存储位置不同:
基本数据类型直接将值保存在值栈中;
包装类型是把对象放在堆中,然后通过对象的引用来调用他们
5、初始值不同:
int的初始值为 0 、 boolean的初始值为false
包装类型的初始值为null
6、使用方式不同:
基本数据类型直接赋值使用就好;
包装类型是在集合如 coolectionMap时使用
why?
我们知道Java是一个面相对象的编程语言,基本类型并不具有对象的性质,为了让基本类型也具有对象的特征,就出现了包装类型(如我们在使用集合类型Collection时就一定要使用包装类型而非基本类型),它相当于将基本类型“包装起来”,使得它具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。
另外,当需要往ArrayList,HashMap中放东西时,像int,double这种基本类型是放不进去的,因为容器都是装object的,这是就需要这些基本类型的包装器类了。
基本数据类型和包装类之间的转换
包装类–>基本数据类型:包装类对象.xxxValue()
基本数据类型–>包装类:new 包装类(基本类型值)
JDK1.5 开始提供了自动装箱(autoboxing)和自动拆箱(autounboxing)功能, 实现了包装类和基本数据类型之间的自动转换
包装类可以实现基本类型和字符串之间的转换,字符串转基本类型:parseXXX(String s);基本类型转字符串:String.valueOf(基本类型)
12、数据库的DDL和DML的用法和区别?
DML(data manipulation language): 它们是SELECT、UPDATE、INSERT、DELETE,就象它的名字一样,这4条命令是用来对数据库里的数据进行操作的语言
DDL(data definition language): DDL比DML要多,主要的命令有CREATE、ALTER、DROP等,DDL主要是用在定义或改变表(TABLE)的结构,数据类型,表之间的链接和约束等初始化工作上,他们大多在建立表时使用
DCL(Data Control Language): 是数据库控制功能。是用来设置或更改数据库用户或角色权限的语句,包括(grant,deny,revoke等)语句。在默认状态下,只有sysadmin,dbcreator,db_owner或db_securityadmin等人员才有权力执行DCL
TCL - Transaction Control Language:事务控制语言,COMMIT - 保存已完成的工作,SAVEPOINT - 在事务中设置保存点,可以回滚到此处,ROLLBACK - 回滚,SET TRANSACTION - 改变事务选项
下表一目了然
13、JDK1.8新特性
最想说的是这个:HashMap在Jdk1.7和1.8中的实现
https://yuanrengu.com/2020/ba184259.html 这个链接针对源码解析,分析了区别。
总结:
1.7中采用数组+链表,1.8采用的是数组+链表/红黑树,即在1.7中链表长度超过一定长度后就改成红黑树存储。
1.7扩容时需要重新计算哈希值和索引位置,1.8并不重新计算哈希值,巧妙地采用和扩容后容量进行&操作来计算新的索引位置。
1.7是采用表头插入法插入链表,1.8采用的是尾部插入法。
在1.7中采用表头插入法,在扩容时会改变链表中元素原本的顺序,以至于在并发场景下导致链表成环的问题;在1.8中采用尾部插入法,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了
下面是了解内容
1、default关键字
在java里面,我们通常都是认为接口里面是只能有抽象方法,不能有任何方法的实现的,那么在jdk1.8里面打破了这个规定,引入了新的关键字default,通过使用default修饰方法,可以让我们在接口里面定义具体的方法实现,如下。
public interface NewCharacter {
public void test1();
public default void test2(){
System.out.println("我是新特性1");
}
}
2、Lambda 表达式
Lambda表达式是jdk1.8里面的一个重要的更新,这意味着java也开始承认了函数式编程,并且尝试引入其中。
首先,什么是函数式编程,引用廖雪峰先生的教程里面的解释就是说:函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的。函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!
简单的来说就是,函数也是一等公民了,在java里面一等公民有变量,对象,那么函数式编程语言里面函数也可以跟变量,对象一样使用了,也就是说函数既可以作为参数,也可以作为返回值了
3、函数式接口
定义:“函数式接口”是指仅仅只包含一个抽象方法的接口,每一个该类型的lambda表达式都会被匹配到这个抽象方法。jdk1.8提供了一个@FunctionalInterface注解来定义函数式接口,如果我们定义的接口不符合函数式的规范便会报错。
4.方法与构造函数引用
jdk1.8提供了另外一种调用方式::,当 你 需 要使用 方 法 引用时 , 目 标引用 放 在 分隔符::前 ,方法 的 名 称放在 后 面 ,即ClassName :: methodName 。例如 ,Apple::getWeight就是引用了Apple类中定义的方法getWeight。请记住,不需要括号,因为你没有实际调用这个方法。方法引用就是Lambda表达式(Apple a) -> a.getWeight()的快捷写法,如下示例。
5、局部变量限制
Lambda表达式也允许使用*变量(不是参数,而是在外层作用域中定义的变量),就像匿名类一样。 它们被称作捕获Lambda。 Lambda可以没有限制地捕获(也就是在其主体中引用)实例变量和静态变量。但局部变量必须显式声明为final,或事实上是final。
为什么局部变量有这些限制?
(1)实例变量和局部变量背后的实现有一个关键不同。实例变量都存储在堆中,而局部变量则保存在栈上。如果Lambda可以直接访问局部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能会在分配该变量的线程将这个变量收回之后,去访问该变量。因此, Java在访问*局部变量时,实际上是在访问它的副本,而不是访问原始变量。如果局部变量仅仅赋值一次那就没有什么区别了——因此就有了这个限制。
(2)这一限制不鼓励你使用改变外部变量的典型命令式编程模式。
6、Date Api更新
1.8之前JDK自带的日期处理类非常不方便,我们处理的时候经常是使用的第三方工具包,比如commons-lang包等。不过1.8出现之后这个改观了很多,比如日期时间的创建、比较、调整、格式化、时间间隔等。这些类都在java.time包下。比原来实用了很多。
7、流
定义:流是Java API的新成员,它允许我们以声明性方式处理数据集合(通过查询语句来表达,而不是临时编写一个实现)。就现在来说,我们可以把它们看成遍历数据集的高级迭代器。此外,流还可以透明地并行处理,也就是说我们不用写多线程代码了。
14、java线程池有哪几类?
(了解内容)
①:FixedThreadPool,特点:固定池子中线程的个数。使用静态方法newFixedThreadPool()创建线程池的时候指定线程池个数。
②:CachedThreadPool(弹性缓存线程池),特点:用newCachedThreadPool()方法创建该线程池对象,创建之初里面一个线程都没有,当execute方法或submit方法向线程池提交任务时,会自动新建线程;如果线程池中有空余线程,则不会新建;这种线程池一般最多情况可以容纳几万个线程,里面的线程空余60s会被回收。
③:SingleThreadPool(单线程线程池),特点:池中只有一个线程,如果扔5个任务进来,那么有4个任务将排队;作用是保证任务的顺序执行。
④:ScheduledThreadpool(定时器线程池)
⑤:WorkStealingPool
⑥:ForkJoinPool