Java

2020-04-25  本文已影响0人  技术灭霸

1、一个字符(英文字母)占多少个字节,一个中文占多少字节?

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的区别

实现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、&& 和 & 的区别、 || 和 | 的区别

|与||的原理同上。短路与 或 短路或的计算效率更高,建议使用。

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()之间的关系

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

9、使用final的意义

1、为方法“上锁”,防止任何继承类改变它的本来含义和实现。设计程序时,若希望一个方法的行为在继承期间保持不变,而且不可被覆盖或改写,就可以采取这种做法。
2、提高程序执行的效率,将一个方法设成final后,编译器就可以把对那个方法的所有调用都置入“嵌入”调用里(内嵌机制)
3、如果一个数据既是static又是final,那么它会拥有一块无法改变的存储空间

10、多态的好处与实现原理

好处

1、提高了代码的维护性(继承保证)
2、提高了代码的扩展性(由多态保证)
多态:同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。在复运行时,可以通过指向基类的指针,来调用实现派生类中的方法。

实现原理

多态是面向对象编程语言的重要特性,它允许基类的指针或引用指向派生类的对象,而在具体访问时实现方法的动态绑定。

Java 对于方法调用动态绑定的实现主要依赖于方法表,但通过类引用调用(invokevitual)和接口引用调用(invokeinterface)的实现则有所不同

11、Java8新特性

函数式接口与Lambda表达式之间的关系:lambda表达式相当于是一个行为,传入函数式接口中,进来实现各种操作,即行为参数化它们的接口内只有一个抽象方法,每一个函数式接口都有@FunctionalInterface注解。

形参列表=>函数体
(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)

image.png

13、如何去设计类和接口(Effective Java)

1、使类和成员的可访问性最小化

尽可能地使每个类或者成员不被外界访问,尽可能最小的访问级别。

2、复合优先于继承

与方法调用不同的是,继承打破了封装性。超类的实现有可能会随着发行版本的不同而有所变化,如果真的发生了变化,子类可能会遭到破坏,即使它的代码完全没有改变。

建议新的类中增加一个私有域,它引现有类的一个实例。这种设计被称做“复合(composition)

3、接口优于抽象类

如果你希望让两个类扩展同一个抽象类,就必须把抽象类放到类型层次结构的高处,以便这两个类的一个祖先成为它的子类。遗憾的是这样做会间接到伤害到类层次,迫使这个公共祖先到所有后代类都扩展这个新的抽象类,无论它对于这些后代类是否合适。

4、优先考虑静态成员类

非静态成员类的每个实例都隐含着与外围类的一个外围实例(enclosing instance)相关联。

14、ArrayList和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中的代码,有以下两种方式:

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"%>基本上就能解决这类乱码问题。

第二种解决方法:

  1. 更改 D:\Tomcat\conf\server.xml,指定浏览器的编码格式为“简体中文”:
    URIEncoding='GBK'
  2. 更改 Java 程序, response.setContentType("text/html; charset=GBK");
  3. 通过byte流修改:name = new String(name.getBytes("iso-8859-1"),"utf-8");
  4. 设置编码格式:post请求:request.setCharacterEncoding("utf-8");
  5. 添加filter过滤器,在web.xml中添加过滤器:它的作用是让浏览器把Unicode字符转换为GBK字符

23、重载

在编译器眼里,方法名称+参数类型+参数个数,组成一个唯一键,称为方法签名。返回值并不是方法签名的一部分,会导致编译出错。

一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。

24、基本数据类型

image.png

25、对象头的内部结构

image.png

26、什么情况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开发六大原则

  1. 单一原则 : 一个类或一个方法只负责一件事情
  2. 里斯替换原则: 子类不应该重写父类已实现的方法,重载不应该比父类的参数更少
  3. 依赖倒置原则: 面向接口编程.(面向接口更能添加程序的可扩展性)
  4. 接口隔离原则: 接口中的方法应该细分,要合理的隔离开不同的功能到不同的接口中.
  5. 迪米特原则: 高内聚低耦合
  6. 开闭原则: 对修改关闭,对扩展开放
    总结: 用抽象构建框架,用实现扩展细节…

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.png

35、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方法引发的内存泄漏

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就会适时的回收父字符串占用的内存空间。

上一篇下一篇

猜你喜欢

热点阅读