基本类型:比较的是值是否相同 引用类型:比较的是地址是否相同,也就是说 new 出来的对象进行 == 比较,始终返回false
默认情况下,如果我们没有对对象的 equals 方法进行重写,那 equals 比较的就是地址 而我们使用的 String 是对 equals 进行了重写,把它变成了值比较 注意:重写时 equals 和 hashCode 必须同时重写 两个对象的 hashCode()相同,则 equals()也一定为 true,对吗 不对,两个对象的 hashCode()相同,equals()不一定 true 因为在散列表中,hashCode()相等即两个键值对的哈希值相等 然而哈希值相等,并不一定能得出键值对相等 代码示例
String str1 = "通话"; String str2 = "重地"; System.out.println(String.format("str1:%d | str2:%d", str1.hashCode(), str2.hashCode())); System.out.println(str1.equals(str2));执行的结果 str1:1179395 | str2:1179395 哈希值相等 false equals结果为false
final 修饰的类叫最终类,该类不能被继承。 final 修饰的方法不能被重写。 final 修饰的变量叫常量,常量必须初始化,初始化之后值就不能被修改
String 字符串常量、StringBuffer 字符串变量(线程安全)、StringBuilder 字符串变量(非线程安全)
String 底层是final修饰的不可变对象,因此在每次对 String 类型进行改变的时候其实都等同于生成了一个新的 String 对象,然后将指针指向新的 String 对象 所以经常改变内容的字符串最好不要用String,因为每次生成对象都会对系统性能产生影响,特别当内存中无引用对象多了以后,JVM 的 GC 就会开始工作,那速度是一定会相当慢的 StringBuilder类则结果就不一样了,每次改变都会对 StringBuilder对象本身进行操作,而不是生成新的对象,再改变对象引用,所以在一般情况下我们推荐使用 StringBuilder,特别是字符串对象经常改变的情况下 而StringBuffer与StringBuilder不同就在于StringBuffer是线程安全的,底层的append方法使用了synchronized关键字进行修饰不一样,因为内存的分配方式不一样。 String str = "i" 的方式,java 虚拟机会将其分配到常量池中; 而 String str = new String("i") 则会被分到堆内存中
重载 参数类型、个数、顺序至少有一个不相同 不能重载只有返回值不同的方法名 存在于父类和子类、同类中 重写 方法名、参数、返回值相同。 子类方法不能缩小父类方法的访问权限。 子类方法不能抛出比父类方法更多的异常(但子类方法可以不抛出异常)。 存在于父类和子类之间。 方法被定义为final不能被重写。
1**.int**是基本数据类型,Integer是包装类,是一个类 2.int类型的变量初始值为0,而Integer包装类初始值为null 3.Integer类的作用 一是在类中拥有很多方法,方便Int和其他数据类型进行转换,比如parseInt()和toString()方法等等 二是向ArrayList或HashMap存数据时不能用基本类型,只能用包装类
(1)抽象类可以有默认的方法实现。接口根本不存在方法的实现 (2)子类使用extends关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现,子类使用关键字implements来实现接口。它需要提供接口中所有声明的方法的实现 (3)抽象类可以有构造器 接口不能有构造器 (4)抽象方法可以有public、protected和default这些修饰符 ,接口方法默认修饰符是public 你不可以使用其它修饰符 (5)抽象类在java语言中所表示的是一种继承关系,一个子类只能继承一个父类,但是可以实现多个接口 (6)如果你往抽象类中添加新的方法,你可以给它提供默认的实现。因此你不需要改变你现在的代码。 如果你往接口中添加方法,那么你必须改变实现该接口的类
传值 实际是将一个值的拷贝传递至方法内部,这个值的原始数据是不会改变的 无论你内部进行的是何种操作,都不会改变这个源数据的值;
传引用 传递进去的则是指向一个对象的地址,那么在方法内部进行实际操作的时候 就很可能会改变该对象的属性值(当然具体是否改变,还需要结合具体的业务)
装箱就是自动将基本数据类型转换为包装类型; 拆箱就是自动将包装类型转换为基本数据类型
是编译器帮我们自动调用了拆装箱的方法,以 Integer/int 为例子 自动装箱就是编译器自动调用了 valueOf(int i) 方法 自动拆箱就是调用了 intValue() 方法,其他基本类型类推
它会首先判断i的大小:如果i小于-128或者大于等于128 就创建一个Integer对象,否则执行SMALL_VALUES[i + 128]
private static final Integer[] SMALL_VALUES = new Integer[256];SMALL_VALUES 它是一个静态的Integer数组对象 也就是说最终valueOf返回的都是一个Integer对象
在循环的时候
Integer sum = 0; for(int i=0; i < 4000; i++){ sum += i; } 上面的代码`sum +=i` 可以看成`sum = sum + i`,在sum被+操作符操作的时候,会对sum进行自动拆箱操作进行数值相加操作,最后发生自动装箱操作转换成Integer对象。其内部变化如下
sum = sum.intValue() + i; Integer sum = new Integer(result); sum为Integer类型,在上面的循环中会创建4000个无用的Integer对象 在这样庞大的循环中,会降低程序的性能并且加重了垃圾回收的工作量
(1)为了实现字符串池 只有当字符串是不可变的,字符串池才有可能实现。字符串池的实现可以在运行时节约很多堆空间,因为不同的字符串变量都指向池中的同一个字符串。但如果字符串是可变的,那么String interning将不能实现,因为这样的话,如果变量改变了它的值,那么其它指向这个值的变量的值也会一起改变。
(2)为了线程安全 因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。 (3)为了实现String可以创建HashCode不可变性 因为字符串是不可变的,所以在它创建的时候HashCode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串
ArrayList 中的数据对象有顺序且可以重复、HashSet 中的数据对象没有顺序且不可以重复
HashSet 底层就是 HashMap,但是 HashSet 添加元素时,只关心 key,不关心 value 因为 HashSet 源码中 value 的值默认就是一个 PRESENT 的 Object类型常量
HashSet 里面存放的是对象的引用,equals是用来判断是否引用同一个对象,所以当两个元素只要满足了equals()时就已经指向同一个对象,也就出现了重复元素。所以是用equals()来判断。
Vector增长率为目前数组长度的100%,而ArrayList增长率为目前数组长度的50% Vector是同步的,而ArrayList不是。如果在迭代的时候想对列表进行改变,你应该使用CopyOnWriteArrayList。 ArrayList比Vector快,它因为有同步,不会过载。
1)ArrayList是动态数组的数据结构,LinkedList基于链表的数据结构 2)当 new ArrayList() 时,默认构造的是大小为 10的一个 Object[] 数组,当数组的元素超过其容量大小后,会扩容为原来的1.5倍,调用的是 Arrays.copyOf() 方法进行扩容 3)ArrayList 源码中最大的数组容量是Integer.MAX_VALUE-8, 对于空出的8位JDK文档中说是避免一些机器内存溢出,减少出错几率,所以少分配
1)ArrayList 对于随机访问元素,向数组尾部添加元素的效率高于LinkedList,但是,删除以及向数组中间添加数据效率低,因为 ArrayList 需要移动数组 2)ArrayList 添加元素源码分析 ArrayList 源码中添加元素时,会判断如果是尾部,则直接返回尾部的索引 size+1,先改变尾部的索引,再将新元素添加至数组位置 size + 1 如果不是尾部添加,则源码中会进行删除、移动数组的操作。而在数组移动复制时,最终将调用System.arraycopy() 方法,所有这一步就会使得效率较 LinkedList 慢 3)LinkedList 基于链表的动态数组,数据添加删除效率高,只需要改变指针指向即可,但是访问数据的平均效率低,需要对链表进行遍历
(1)ArrayList通过foreach迭代是调用的其内部类iterator的next方法。 (2)如果通过foreach循环,要去删除某些元素,只能通过迭代器删除 (3)因为迭代器删除后会对modCount设值,不会再循环过程因为modCount值不相等而抛出异常 (4)如果是通过ArrayList的删除,则内部迭代器iterator的属性modCount却没有得到更新,所以会抛异常 CopyOnWriteArrayList
不使用锁,而是在写数据的时候利用拷贝的副本来执行 (1)将要操作的共享变量 object[] 数组使用volatile关键字修饰 (2)让写线程在往集合中添加数据的时候,先拷贝存储的数组,然后添加元素到拷贝好的数组中,再用现在的数组去替换成员变量的数组 (3)读线程根据 volatile关键字的可见性原则,就能够感知到变化 下面是JDK里的 CopyOnWriteArrayList 的源码
// 这个数组是核心的,因为用volatile修饰了 // 只要把最新的数组对他赋值,其他线程立马可以看到最新的数组 private transient volatile Object[] array; // 写操作 public boolean add(E e) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; // 对数组拷贝一个副本出来 Object[] newElements = Arrays.copyOf(elements, len + 1); // 对副本数组进行修改,比如在里面加入一个元素 newElements[len] = e; // 然后把副本数组赋值给volatile修饰的变量 setArray(newElements); return true; } finally { lock.unlock(); } } // 读线程根据 volatile关键字的可见性原则,就能够感知到变化 private E get(Object[] a, int index) { // 最简单的对数组进行读取 return (E) a[index]; }(1)CopyOnWrite的机制虽然是线程安全的,但是在add操作的时候需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可导致young gc或者full gc,所以使用到这个集合的时候尽量不要出现频繁的添加操作 (2)而且在迭代的时候如果数据量太多的时候,实时性可能就差距很大了。在多读取,少添加的时候,他的效果还是不错的(数据量大无所谓,只要你不添加,他都是好用的)
Comparable是一个内比较器,实现了Comparable接口的一个compareTo方法,如果开发者add进入一个Collection的对象想要Collections的sort方法帮你自动进行排序的话,那么这个对象必须实现Comparable接口
Comparator可以认为是一个外比较器,个人认为有两种情况可以使用实现Comparator接口的方式: 1、一个对象不支持自己和自己比较(没有实现Comparable接口),但是又想对两个对象进行比较。 2、一个对象实现了Comparable接口,但是开发者认为compareTo方法中的比较方式并不是自己想要的那种比较方式。 Comparator接口里面有一个compare方法,方法有两个参数T o1和T o2,是泛型的表示方式,分别表示待比较的两个对象,方法返回值和Comparable接口一样是int
HashMap把Hashtable的contains方法去掉了,改成containsValue和containsKey,因为contains方法容易让人引起误解 HashMap允许将null作为一个entry的key或者value,而Hashtable不允许 HashTable的方法是Synchronize修饰的,是线程安全的,也就是说是同步的 HashMap是线程序不安全的,不是同步的,由于非线程安全,在只有一个线程访问的情况下,效率要高于Hashtable
在HashMap提供的遍历方式中,有 Iterator 和 keySet 方式,根据阿里开发手册,不建议使用这两种方式,因为会迭代两次,降低性能。 JDK8以后 Map 接口中增加了默认方法 forEach(BiConsumer<? super K,? super V> action) ,BiConsumer 为函数式接口,建议使用这种方式
map.forEach((key,value)->{ System.out.println(key+"---"+value); });为什么HashMap使用红黑树而不是AVL树 红黑树牺牲了一些查找性能,但其本身并不是完全平衡的二叉树,因此插入删除操作效率略高于AVL树 AVL树用于自平衡的计算牺牲了插入删除性能,但是因为最多只有一层的高度差,查询效率会高一些
image LinkedHashMap继承于HashMap,LinkedHashMap是有序的,且默认为插入顺序 LinkedHashMap是在HashMap的基础上,多了一个双向链表来维持顺序
在LinkedHashMap的get()方法中,我们每次获取元素的时候,都要调用afterNodeAccess(e)将元素移动到尾部,在插入数据的时候,如果removeEldestEntry(first)返回true,按照LRU策略,那么会删除头节点
(1)当某个位置的数据被命中,通过调整该数据的位置,将其移动至尾部 (2)新插入的元素也是直接放入尾部(尾插法) (3)这样一来,最近被命中的元素就向尾部移动,那么链表的头部就是最近最少使用的元素所在的位置
这时如果多个线程过来,线程1要put的位置是数组1[5],线程2要put的位置是数组2[21],put的数组位置不同,则不会影响,如果put的是同一个数组,此时就是串行处理了,并发性肯定会降低
=================================================== (几年面试的总结,有自己的,有网上看各位大佬总结的,归纳到一起,没有一次写完,后续加更,可能有些理解不是很到位,不完整,欢迎大佬指点,指出 错误的地方) 常见JAVA面试题总结<2020 java面试必备>(二)多线程
常见JAVA面试题总结<2020 java面试必备>(三)JVM
常见JAVA面试题总结<2020 java面试必备>(四)设计模式
常见JAVA面试题总结<2020 java面试必备>(五) 网络