1、一个字符(英文字母)占多少个字节,一个中文占多少字节?
- 一个字符占1个字节(GBK、ASCII、UTF-8)
- 一个中文占 2 个字节(GBK、ASCII)
- 一个中文占 3 个字节(UTF-8)
2、 Double是怎么比较两个值的大小
推荐使用BigDecimal
double a = 0.01;
double b = 0.001;
BigDecimal data1 = new BigDecimal(a);
BigDecimal data2 = new BigDecimal(b);
System.out.print(new DoubleCompare().compare(data1, data2));
3、==和equals的区别?实现equals要注意哪些东西?
==和equals的区别
- ==:判断两个字符串在内存中首地址是否相同,即判断两者是否是同一个字符串对象
- equles():如果没有重写equals()方法比较的是对象的地址,因为对Object来说对象没有什么属性可以比较,只能比较最底层的地址。
而如果重写equals()方法时,该方法的对象因为是Object的子类,所以调用时会调用子类对象里面的方法.所以只有重写equals()方法后,两者比较的才是内容.或者说重写可以使自己定义比较的规则,不想按照地址去比较.
实现equals要注意哪些东西?
1、自反性:对于任何非空引用x,x.equals(x)应该返回true。
2、对称性:对于任何引用x和y,如果x.equals(y)返回true,那么y.equals(x)也应该返回true。
3、传递性:对于任何引用x、y和z,如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)也应该返回true。
4、一致性:如果x和y引用的对象没有发生变化,那么反复调用x.equals(y)应该返回同样的结果。
5、非空性:对于任意非空引用x,x.equals(null)应该返回false。
4、&& 和 & 的区别、 || 和 | 的区别
- &:在算术运算时分别计算表达式两边的结果,再作&运算。在位运算时&表示按位与
- &&:短路与运算,先计算左边的表达式,如果结果是false,那么不用计算右边表达式,直接返回false。
|与||的原理同上。短路与 或 短路或的计算效率更高,建议使用。
5、HashMap
HashMap查询时间复杂度
hashMap除了超过负载因子的时候会扩容,还有什么情况下会扩容?
一种是元素达到阀值了,一种是HashMap准备树形化但又发现数组太短(没有达到64)
HashMap是如何存储空key的?
空key的hash值为0,创建hash为0,key为null的node。
6、ConcurrentModificationException异常出现的原因
原因:如果modCount不等于expectedModCount,则抛出ConcurrentModificationException异常。
关键点就在于:调用list.remove()方法导致modCount和expectedModCount的值不一致。
1、在单线程环境下的解决办法
使用iterator删除,并且调用iterator的remove方法,不是list的remove方法
2、在多线程环境下的解决方法
1、在使用iterator迭代的时候使用synchronized或者Lock进行同步;
2、使用并发容器CopyOnWriteArrayList代替ArrayList和Vector。
7、equals()与hashCode()之间的关系
- 如果两个对象equals()方法相等则它们的hashCode返回值一定要相同,如果两个对象的hashCode返回值相同,但它们的equals()方法不一定相等。
- hashCode()的作用是为了提高在散列结构存储中查找的效率
- Java中重写equals()方法时尽量要重写hashCode()方法的原因:声明相等对象必须具有相等的哈希码,包括 HashMap、HashSet、Hashtable 等
8、ConcurrentHashMap
Segment(分段锁)技术:将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问,能够实现真正的并发访问。
1.8的优化:采用Node + CAS + Synchronized来保证并发安全进行实现,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率。
CAS主要用于修改sizeCtl的值
sizeCtl :默认为0,用来控制table的初始化和扩容操作。
**-1 **代表table正在初始化
**-N **表示有N-1个线程正在进行扩容操作
其余情况:
1、如果table未初始化,表示table需要初始化的大小。
2、如果table初始化完成,表示table的容量,默认是table大小的0.75倍,居然用这个公式算0.75(n - (n >>> 2))。
Synchronized
- 把新的Node节点按链表或红黑树的方式插入到合适的位置,这个过程采用同步内置锁实现并发
- 生成树节点的代码块是同步的,进入同步代码块之后,再次验证table中index位置元素是否被修改过
9、使用final的意义
1、为方法“上锁”,防止任何继承类改变它的本来含义和实现。设计程序时,若希望一个方法的行为在继承期间保持不变,而且不可被覆盖或改写,就可以采取这种做法。
2、提高程序执行的效率,将一个方法设成final后,编译器就可以把对那个方法的所有调用都置入“嵌入”调用里(内嵌机制)
3、如果一个数据既是static又是final,那么它会拥有一块无法改变的存储空间
10、多态的好处与实现原理
好处
1、提高了代码的维护性(继承保证)
2、提高了代码的扩展性(由多态保证)
多态:同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。在复运行时,可以通过指向基类的指针,来调用实现派生类中的方法。
实现原理
多态是面向对象编程语言的重要特性,它允许基类的指针或引用指向派生类的对象,而在具体访问时实现方法的动态绑定。
Java 对于方法调用动态绑定的实现主要依赖于方法表,但通过类引用调用(invokevitual)和接口引用调用(invokeinterface)的实现则有所不同
11、Java8新特性
函数式接口与Lambda表达式之间的关系:lambda表达式相当于是一个行为,传入函数式接口中,进来实现各种操作,即行为参数化它们的接口内只有一个抽象方法,每一个函数式接口都有@FunctionalInterface注解。
- 使原本需要用匿名类实现接口来传递行为,现在通过 Lambda 可以更直观的表达。
- Lambda 表达式是一个匿名函数,即没有函数名的函数。有些函数如果只是临时一用,而且它的业务逻辑也很简单时,就没必要非给它取个名字不可。
- 允许把函数作为一个方法的参数
形参列表=>函数体
(parameters) -> expression
image.png
-> 返回
: 等于
map() 类型转换、映射
12、exception和error区别
都继承自Throwable类
Exception:
1.可以是可被控制(checked) 或不可控制的(unchecked)。
2.表示一个由程序员导致的错误。
3.应该在应用程序级被处理。
比如 NullPointerException、IndexOutOfBoundsException、 IOException、ClassNotFoundException
Error:
1.总是不可控制的(unchecked)。
2.经常用来用于表示系统错误或低层资源的错误。
3.如何可能的话,应该在系统级被捕捉。
比如栈溢出(StackOverflowError)、堆溢出(OutOfMemoryError:java heap space)
13、如何去设计类和接口(Effective Java)
1、使类和成员的可访问性最小化
尽可能地使每个类或者成员不被外界访问,尽可能最小的访问级别。
2、复合优先于继承
与方法调用不同的是,继承打破了封装性。超类的实现有可能会随着发行版本的不同而有所变化,如果真的发生了变化,子类可能会遭到破坏,即使它的代码完全没有改变。
建议新的类中增加一个私有域,它引现有类的一个实例。这种设计被称做“复合(composition)
3、接口优于抽象类
如果你希望让两个类扩展同一个抽象类,就必须把抽象类放到类型层次结构的高处,以便这两个类的一个祖先成为它的子类。遗憾的是这样做会间接到伤害到类层次,迫使这个公共祖先到所有后代类都扩展这个新的抽象类,无论它对于这些后代类是否合适。
4、优先考虑静态成员类
非静态成员类的每个实例都隐含着与外围类的一个外围实例(enclosing instance)相关联。
14、ArrayList和LinkedList的区别
- 底层实现:ArrayList是实现了基于动态数组的数据结构,而LinkedList是基于链表的数据结构,ArrayList需要扩容、LinkedList不需要
- 时间复杂度:对于随机访问get和set,ArrayList要优于LinkedList,因为LinkedList要移动指针
- 使用场景:LinkedList是个双向链表,它同样可以被当作栈、队列或双端队列来使用。
15、HashMap的hash函数原理
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
Java 8中这一步做了优化,只做一次16位右位移异或混合,而不是四次,但原理是不变的。
优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的,主要是从速度、功效、质量来考虑的
16、为什么notify和wait方法必须在synchronized方法中使用?
1、依赖锁对象的监视器monitor
这是因为调用这三个方法之前必须拿要到当前锁对象的监视器monitor对象,也就是说notify/notifyAll和wait方法依赖于monitor对象,又因为monitor存在于对象头的Mark Word中(存储monitor引用指针),而synchronized关键字可以获取monitor ,所以,notify/notifyAll和wait方法必须在synchronized代码块或者synchronized方法中调用。
2、避免lost wake up问题
因为会导致lost wake up问题,说白了就唤不醒消费者
image.png
为了避免出现这种lost wake up问题,Java强制我们的wait()/notify()调用必须要在一个同步块中。
17、finally方法一定会被执行么?
java中,如果想要执行try中的代码之后,不允许再执行finally中的代码,有以下两种方式:
- 使用System.exit(1)来退出虚拟机
- 把当前执行trycatchfinally代码的线程设置为守护线程
18、# 为什么volatile能保证可见性?
内存屏障(memory barrier) 是一个CPU指令。基本上,它是这样一条指令:
a) 确保一些特定操作执行的顺序;
b) 影响一些数据的可见性(可能是某些指令执行后的结果)。编译器和CPU可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。插入一个内存屏障, 相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。内存屏障另一个作用是强制更新一次不同CPU的缓存。例如,一个写屏障会 把这个屏障前写入的数据刷新到缓存,这样任何试图读取该数据的线程将得到最新值,而不用考虑到底是被哪个cpu核心或者哪颗CPU执行的。
内存屏障和volatile什么关系?上面的虚拟机指令里面有提到,如果你的字段是volatile,Java内存模型将在写操作后插入一个写屏障 指令,在读操作前插入一个读屏障指令。这意味着如果你对一个volatile字段进行写操作,你必须知道:
1、一旦你完成写入,任何访问这个字段的线程将 会得到最新的值。
2、在你写入前,会保证所有之前发生的事已经发生,并且任何更新过的数据值也是可见的,因为内存屏障会把之前的写入值都刷新到缓存。
明白了内存屏障这个CPU指令,回到前面的JVM指令:从Load到store到内存屏障,一共4步,其中最后一步jvm让这个最新的变量的值在所有线程可见,也就是最后一步让所有的CPU内核都获得了最新的值,但中间的几步(从Load到Store)是不安全的,中间如果其他的CPU修改了值将会丢失。
所以volatile不能保证i++操作的原子性
19、值传递和引用传递的区别?
https://mp.weixin.qq.com/s/4efxpvxOAzg1E4eLIsRLiw
值传递:
在方法被调用时,实参通过形参把它的内容副本传入方法内部,此时形参接收到的内容是实参值的一个拷贝,因此在方法内对形参的任何操作,都仅仅是对这个副本的操作,不影响原始值的内容。
public static void valueCrossTest(int age, float weight){
System.out.println("传入的age:" + age);
System.out.println("传入的weight:" + weight);
age = 33;
weight = 89.5f;
System.out.println("方法内重新赋值后的age:" + age);
System.out.println("方法内重新赋值后的weight:" + weight);
}
public static void main(String[] args) {
int a = 25;
float w = 77.5f;
valueCrossTest(a, w);
System.out.println("方法执行后的age:" + a);
System.out.println("方法执行后的weight:"+w);
}
传入的age:25
传入的weight:77.5
方法内重新赋值后的age:33
方法内重新赋值后的weight:89.5
方法执行后的age:25
方法执行后的weight:77.5
image.png
只是改变了当前栈帧(valueCrossTest方法所在栈帧)里的内容,当方法执行结束之后,这些局部变量都会被销毁,mian方法所在栈帧重新回到栈顶,成为当前栈帧,再次输出a和w时,依然是初始化时的内容。
值传递传递的是真实内容的一个副本,对副本的操作不影响原内容,也就是形参怎么变化,不会影响实参对应的内容。
引用传递:
”引用”也就是指向真实内容的地址值,在方法调用时,实参的地址通过方法调用被传递给相应的形参,在方法体内,形参和实参指向通愉快内存地址,对形参的操作会影响的真实内容。
public static void PersonCrossTest(Person person){
System.out.println("传入的person的name:"+person.getName());
person.setName("我是张小龙");
System.out.println("方法内重新赋值后的name:"+person.getName());
}
public static void main(String[] args) {
Person p = new Person();
p.setName("我是马化腾");
p.setAge(45);
PersonCrossTest(p);
System.out.println("方法执行后的name:"+p.getName());
}
传入的person的name:我是马化腾
方法内重新赋值后的name:我是张小龙
方法执行后的name:我是张小龙
可以看出,person经过personCrossTest()方法的执行之后,内容发生了改变,这印证了上面所说的“引用传递”,对形参的操作,改变了实际对象的内容。
修改一下
public static void PersonCrossTest(Person person){
System.out.println("传入的person的name:"+person.getName());
person=new Person();//加多此行代码
person.setName("我是张小龙");
System.out.println("方法内重新赋值后的name:"+person.getName());
}
传入的person的name:我是马化腾
方法内重新赋值后的name:我是张小龙
方法执行后的name:我是马化腾
JVM需要在堆内另外开辟一块内存来存储new Person(),假如地址为“xo3333”,那此时形参person指向了这个地址,假如真的是引用传递,那么由上面讲到:引用传递中形参实参指向同一个对象,形参的操作会改变实参对象的改变。
20、Java中8种基本数据类型是哪些?
byte(1)-> boolean(1) -> short(2)-> char(2)-> int(4)-> float(4)-> long(8)-> double(8)
21、Java中文乱码原理和解决方法
Java乱码主要有两种原因(都和字节流有关):
1、Java和JSP源文件的保存方式是基于字节流的,如果Java和JSP编译成class文件 过程中,使用的编码方式与源文件的编码不一致,就会出现乱码。
2、Java程序与这些 媒介交互(如数据库,文件,流等的存储方式都是基于字节流的)时就会发生字符(char)与字节(byte)之间的转换
第一种解决方法:
基于这种乱码,建议在Java文件中尽量不要写中文(注释部分不参与编译,写中文没关系), 如果必须写的话,尽量手动带参数-ecoding GBK或-ecoding gb2312编译;对于JSP,在文件头加上<%@ page contentType="text/html;charset=GBK"%>或<%@ page contentType="text/html;charset=gb2312"%>基本上就能解决这类乱码问题。
第二种解决方法:
- 更改 D:\Tomcat\conf\server.xml,指定浏览器的编码格式为“简体中文”:
URIEncoding='GBK' - 更改 Java 程序, response.setContentType("text/html; charset=GBK");
- 通过byte流修改:name = new String(name.getBytes("iso-8859-1"),"utf-8");
- 设置编码格式:post请求:request.setCharacterEncoding("utf-8");
- 添加filter过滤器,在web.xml中添加过滤器:它的作用是让浏览器把Unicode字符转换为GBK字符
23、重载
在编译器眼里,方法名称+参数类型+参数个数,组成一个唯一键,称为方法签名。返回值并不是方法签名的一部分,会导致编译出错。
一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。
24、基本数据类型
image.png25、对象头的内部结构
image.png26、什么情况finally不会执行
1、没有进入try代码块。
2、进入try代码块 , 但是代码运行中出现了死循环或死锁状态。
3、进入try代码块, 但是执行了 System.exit()操作。
注意, finally 是在 return 表达式运行后执行的 , 此时将要 return 的结果 已 经被暂 存起来 , 待 finally 代码块执行结束后再将之前暂存的结果返回
private static int test1() {
int tmp = 10000;
try {
throw new Exception();
} catch (Exception e) {
return ++tmp;
} finally {
tmp = 99999;
}
}
此方法最终的返回值是 10001 ,而不是 99999。
相对在 finally 代码块中赋值,更加危险的做法是在 finally块中使用 return 操作,这样的代码会使返回值变得非常不可控。
private static int test1() {
int x = 1;
int y = 10;
int z = 100;
try {
return ++x;
} catch (Exception e) {
return ++y;
} finally {
return ++z;
}
}
( 1 )最后 return 的功件是由 finally 代码块巾的 return ++z 完成的,所以为法返 回的结果是 101。
( 2 )语旬 return ++x 中的++x 被成功执行,所以运行结果是x=2。
( 3 ) 如果有异常抛出 ,那么运行结果将会是 y =11,而 x=1;
finally代码块中使用 return语旬,使返回值的判断变得复杂,所以避免返回值不
可控,我们不要在 finally代码块中使用 return语句。
27、集合
我们再回到之前 sort()方法中的 TimSort 算法 ,
是归并排序( Merge Sort )与插入排序( Insertion Sort )优化后的排序算法。
分析Comparable接口的排序原理(二叉树中序排序)
实际上比较器的操作,就是经常听到的二叉树的排序算法。通过二叉树进行排序,之后利用中序遍历的方法把内容依次读取出来。
image.png
排序的基本原理,使用第一个元素作为根节点,之后如果后面的内容比根节点要大,则放在左子树,如果内容比根节点的内容要大,则放在右子树。
然后以中序遍历(左根右)输出!
28、hashCode 和 equals
( 1 )如果两个对象的 equals 的结果是相等的 . 则两个对象的 hashCode 的返回值也必须是相同的。
( 2 )任何时候 覆写 equals, 都必须同时覆写 hashCode。
29、fail-fast机制
这种机制经常出现在多线程环境下 , 当前线程会维护一个计数比较器, 即 expectedModCount, 记录已经修改的次数。在进入遍历前, 会把实时修改次数 modCount 赋值给 expectedModCount,如果这两个数据不相等 , 则抛出异常。
Iterator、COW(Copy-on-write)是 fail-safe机制的
30、JAVA开发六大原则
- 单一原则 : 一个类或一个方法只负责一件事情
- 里斯替换原则: 子类不应该重写父类已实现的方法,重载不应该比父类的参数更少
- 依赖倒置原则: 面向接口编程.(面向接口更能添加程序的可扩展性)
- 接口隔离原则: 接口中的方法应该细分,要合理的隔离开不同的功能到不同的接口中.
- 迪米特原则: 高内聚低耦合
- 开闭原则: 对修改关闭,对扩展开放
总结: 用抽象构建框架,用实现扩展细节…
31、HashMap和LinkedHashMap的区别
HashMap的无序其实也有迹可循, 即按照桶下标先后排序;如果有哈希碰撞的情况,则同一个桶位置按照链表先后顺序输出。键只能允许为一条为空,value可以允许为多条为空。
LinkedHashMap的有序是因为维护了双向链表。键和值都不可以为空。
32、HashMap和TreeMap的区别
HashMap:数组方式存储key/value,线程非安全,允许null作为key和value,key不可以重复,value允许重复,不保证元素迭代顺序是按照插入时的顺序,key的hash值是先计算key的hashcode值,然后再进行计算,每次容量扩容会重新计算所以key的hash值,会消耗资源,要求key必须重写equals和hashcode方法
默认初始容量16,加载因子0.75,扩容为旧容量乘2,查找元素快,如果key一样则比较value,如果value不一样,则按照链表结构存储value,就是一个key后面有多个value;
TreeMap:基于红黑二叉树的NavigableMap的实现,线程非安全,不允许null,key不可以重复,value允许重复,存入TreeMap的元素应当实现Comparable接口或者实现Comparator接口,会按照排序后的顺序迭代元素,两个相比较的key不得抛出classCastException。主要用于存入元素的时候对元素进行自动排序,迭代输出的时候就按排序顺序输出
33、Java8 HashMap扩容时为什么不需要重新hash?
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
可以看到它是通过将数据的hash与扩容前的长度进行与操作,根据e.hash & oldCap
的结果来判断,如果是0,说明位置没有发生变化,如果不为0,说明位置发生了变化,而且新的位置=老的位置+老的数组长度。
比如数据B它经过hash之后的值为 1111,在扩容之前数组长度是8,数据B的位置是:
(n-1)&hash = (8-1) & 1111 = 111 & 1111 = 0111
扩容之后,数组长度是16,重新计算hash位置是:
(n-1)&hash = (16-1) & 1111 = 1111 & 1111 = 1111
可见数据B的位置发生了变化,同时新的位置和原来的位置关系是:
新的位置(1111)= 1000+原来的位置(0111)=原来的长度(8)+原来的位置(0111)
继续看一下e.hash & oldCap的结果
e.hash & oldCap = 1111 & 8 = 1111 & 1000 = 1000 (!=0)
34、HashMap的put()方法流程
image.png35、Java集合类框架的基本接口有哪些?
总共有两大接口:Collection 和Map ,一个元素集合,一个是键值对集合; 其中List和Set接口继承了Collection接口,一个是有序元素集合,一个是无序元素集合; 而ArrayList和 LinkedList 实现了List接口,HashSet实现了Set接口,这几个都比较常用; HashMap 和HashTable实现了Map接口,并且HashTable是线程安全的,但是HashMap性能更好;
36、四大引用
引用类型 | 回收时机 | 使用场景 |
---|---|---|
强引用 | 不回收 | 创建对象实例 |
软引用 | 内存不足时 | 图片缓存 |
弱引用 | 垃圾回收 | WeakHashMap,维护一种非强制的映射关系 |
虚引用 | Unknow | 跟踪对象垃圾回收的活动 |
37、final的作用?
修饰变量时,不能被修改了,修改就报错
修饰List时,可以添加和删除元素,值可以改变,但引用不能改变。不能再将这个list变量指向其他的List实例化对象了,即不能再出现list = new ArrayList(); 的代码。
38、Java中由substring方法引发的内存泄漏
- 内存溢出(out of memory ):通俗的说就是内存不够用了,比如在一个无限循环中不断创建一个大的对象,很快就会引发内存溢出。
- 内存泄漏(leak of memory):是指为一个对象分配内存之后,在对象已经不在使用时未及时的释放,导致一直占据内存单元,使实际可用内存减少,就好像内存泄漏了一样。
substring(int beginIndex, int endndex )是String类的一个方法,但是这个方法在JDK6和JDK7中的实现是完全不同的(虽然它们都达到了同样的效果)。在JDK1.6中不当使用substring会导致严重的内存泄漏问题。
String str = "abcdefghijklmnopqrst";
String sub = str.substring(1, 3);
str = null;
这段简单的程序有两个字符串变量str、sub。sub字符串是由父字符串str截取得到的,假如上述这段程序在JDK1.6中运行,我们知道数组的内存空间分配是在堆上进行的,那么sub和str的内部char数组value是公用了同一个,也就是上述有字符a~字符t组成的char数组,str和sub唯一的差别就是在数组中其实beginIndex和字符长度count的不同。在第三句,我们使str引用为空,本意是释放str占用的空间,但是这个时候,GC是无法回收这个大的char数组的,因为还在被sub字符串内部引用着,虽然sub只截取这个大数组的一小部分。当str是一个非常大字符串的时候,这种浪费是非常明显的,甚至会带来性能问题,解决这个问题可以是通过以下的方法:
String str = "abcdefghijklmnopqrst";
String sub = str.substring(1, 3) + "";
str = null;
利用的就是字符串的拼接技术,它会创建一个新的字符串,这个新的字符串会使用一个新的内部char数组存储自己实际需要的字符,这样父数组的char数组就不会被其他引用,令str=null,在下一次GC回收的时候会回收整个str占用的空间。但是这样书写很明显是不好看的,所以在JDK7中,substring 被重新实现了。
在JDK7中改进了substring的实现,它实际是为截取的子字符串在堆中创建了一个新的char数组用于保存子字符串的字符。这样子字符串和父字符串也就没有什么必然的联系了,当父字符串的引用失效的时候,GC就会适时的回收父字符串占用的内存空间。
网友评论