每日面试题

57. 什么是反射?
  • Java提供了反射机制可以在程序运行时访问任意一个类的对象、方法、属性
58. 什么是 java 序列化?什么情况下需要序列化?
  • Java提供的一种保存对象状态的机制
  • 对象需要网络传输时
  • 对象需要持久化至磁盘中时
59. 动态代理是什么?有哪些应用?
  • 动态代理可以在程序运行时生成代理对象,对被代理对象进行一些增强操作
  • Spring AOP
  • 给方法加日志、事物等代码
60. 怎么实现动态代理?
  • 定义共同的接口
  • 实现InvocationHandler,并在实现类里持有被代理对象
  • 通过Proxy.newProxyInstance()方法创建代理对象

String

String的特性

  • final修饰,不可被修改
  • 字符串是常量,用双引号引起来表示。他们的值在创建之后不能更改
  • String对象的字符内容是存储在一个字符数组value[]中的
  • 通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串声明在字符串常量池中

String不可变性的体现

	public void test () {
		String s1 = "abc";//字面量的定义方式,字符串会保存在方法区的字符串常量池内
		String s2 = "abc";//此时常量池已经存在abc字符串,则直接将s2指向字符串常量池内的abc
		System.out.println(s1 == s2);// 返回true,说明亮着地址值相同,引用的是同一个对象。
		s1 = "hello";// 此时在字符串常量池内新建一个hello字符串,将s1的引用指向新的hello。不可在原有字符串上进行修改

		String s3 = "abc";
		s3 += "def";
		System.out.println(s3);// 此时字符串常量池新建一个"abcdef"

		String s4 = "abc";
		String s5 = s3.replace('a','m');// 还是在字符串常量池新建一个mbc字符串
		System.out.println(s4);
		System.out.println(s5);
	}
  • 当对字符串重新赋值时,需要重新指定内存区域赋值,不能使用原有的value进行赋值
  • 当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value
  • 当调用String的replace()方法修改指定的字符或字符串时,也必须重新指定内存区域进行赋值

以下代码输出什么结果?为什么?

	public void test () {
		String str1 = "abc";
		String str2 = new String("abc");
		System.out.println(str1 == str2);
	} 
  • false
  • 他们的内存分配方式不一样
  • str1指向的是字符串常量池内的字符串
  • str2指向的是堆内存中的字符串对象
  • str2创建了两个对象,一个是堆空间的new结构,一个是字符串常量池中的字符串

以下代码输出什么结果?

	public void test () {
		String s1 = "JavaEE";
		String s2 = "Hadoop";

		String s3 = "JavaEEHadoop";
		String s4 = "JavaEE" + "Hadoop";
		String s5 = s1 + "Hadoop";
		String s6 = s5.intern();

		System.out.println(s3 == s4);//true
		System.out.println(s3 == s5);//false
		System.out.println(s3 == s6);//true
	}
  • 常量与常量拼接接的结果就在常量池。且常量池中不会存在相同内容的常量
  • intern()方法返回常量池中的数据
  • 只要其中一个是变量,结果就在堆中

JVM中字符串常量池存放位置说明

  • JDK1.6及以前字符串常量池存储在方法区(永久区)
  • JDK1.7字符串常量池存储在堆空间
  • JDK1.8字符串常量池存储在方法区(元空间)

String、StringBuffer、StringBuilder

  • String 不可变字符序列;StringBuffer、StringBuilder可变字符串
  • StringBuffer线程安全,效率低;StringBuilder线程不安全,效率低。

StringBuffer、StringBuilder扩容

  • 如果添加的数据底层数组放不下了,那就需要扩容底层的数组。默认情况下,扩容为原来容量的2倍+2,同时将原有的数组元素复制到新的数组中。

String常见算法题目

将字符串内指定字符串反转,例如:"abcdefg"反转为"abfedcg",将cdef反转

  • 思路一,转换为char[],遍历交换反转的部分
	public String reverse(String str, int start, int end) {
 		
		char[] arr = str.toCharArray();
		for(int i = start, j = end; i < j; i++,j--) {
			char temp = arr[i];
			arr[i] = arr[j];
			arr[j] = temp;
		}
		return new String(arr);
	}
  • 思路二,字符串拼接
	public String reverse(String str, int start, int end) {
		// 前半段不需要反转的部分
		String reverseStr = str.substring(0,start);
		// 倒序遍历拼接需要反转的部分
		for(int i = end; i >=start; i--) {
			reverseStr += str.charAt(i);
		}
		//拼接后半段不需要反转的部分
		reverseStr += str.substring(end+1);
		return reverseStr;
	}

获取一个字符串在另一个字符串中出现的次数

  • 思路:使用indexOf()方法,返回不是-1则说明存在,接着取之后的部分,直到返回-1
	public int getCount(String mainStr,String subStr) {
		
		int count = 0;
		if(mainStr !=null && subStr != null && mainStr.length() >= subStr.length()){
			int index = 0;
			while((index = mainStr.indexOf(subStr,index)) != -1) {
				
				count++;
				index += subStr.length();	
			}
		}
		return count;
	}

集合

ArrayList源码分析

  • 底层使用数组实现
  • 默认长度10
  • 默认扩容为原来的1.5倍,将原有数组中的数据复制到新的数组中
// 扩容代码
// int newCapacity = oldCapacity + (oldCapacity >> 1);

JDK8中ArrayList的变量

  • 默认无参构造方法时没有创建对象数组,只有在第一次调用add方法时才新建长度为10的默认数组

LinkedList源码分析

  • 底层使用双向链表实现,可以知道当前元素的上一个元素(prev)和下一个元素(next)。内部保存last、first两个元素
  • LinkedList list = new LinkedList(); 内部声明了Node类型的first和last属性,默认值为null
  • list.add(1);第一次创建first元素,将该元素添加至last元素尾部,同时将该元素设为last

Vector与ArrayList之间的区别?

  • Vector线程安全,ArrayList线程不安全
  • Vector每次扩容为原来的两倍,ArrayList每次扩容为之前的1.5倍

Set

  • |---- Collection接口:单列集合,用来存储一个一个的对象
    • |---- Set接口:存储无序的、不可重复的数据
      • |---- HashSet: 作为Set接口的主要实现类;线程不安全的:可以存储null值
        • |---- LinkedHashSet:作为HashSet的子类;遍历其内部数据时,可以按照添加的顺序遍历
    • |---- TreeSet:放入TreeSet中的数据可以按照添加元素指定属性进行排序

HashSet的无序性

  • 无序性不等于随机性。存储的数据在底层数组中并非按照数组索引的顺序添加。是根据添加数据的HashCode决定的。
  • 不可重复性保证当相同的元素只能添加一个。

HashSet添加元素的过程

  • 通过添加元素a的哈希值计算出存放在底层数组的下标如果数组该位置已经存在元素b(链表),则先比较两个元素的哈希值是否相同,如果相同则调用equals方法判断(equals返回false才添加成功),否则存放至链表中。
  • JDK7中新添加的元素放在链表头部
  • JDK8中新添加的元素放在链表尾部

LinkedHashSet

  • 在原有HashSet的基础上添加数据的同时每个数据还维护了两个引用,记录了数据添加的顺序。
  • 对于频繁的遍历效率要高一些

TreeSet

  • 要求存入的数据类型全部相同
  • 两种排序方式:自然排序(实现Compareable接口)和定制排序(创建Comparetor)
    • 自然排序比较两个对象相同的标准为:compareTo()返回0,不再是equals()
    • 定制排序中比较两个对象的标准是compare返回0

Map

  • |---- Map:双列数据,存储key-value的数据
    • |---- HashMap:作为Map的主要实现类,线程不安全,效率高;存储null的key和value
      • |---- LinkedHashMap:保证在遍历map元素时,可以按照添加的顺序实现遍历。在原有的基础上添加了一对指针指向前一个元素和后一个元素,对于频繁的遍历时效率高于HashMap
    • |---- Hashtable:古老的实现类;线程安全效率低;不能存储null的key和value
      • |---- Properties:常用来处理配置文件。key和value都是String类型
    • |---- TreeMap:保证按照添加的key-value进行排序,实现排序遍历

HashMap底层实现原理

  • HashMap map = new HashMap();
    • 在实例化以后,底层创建了一个长度为16的Entry[]数组
  • map.put(key,value)
    • 首先调用key所在类的hashCode()方法获得哈希值,该值根据某种算法计算出在Entry数组中的存放位置
    • 判断
      • 如果此位置上没有数据,此时value添加成功
      • 如果此位置上的数据不为空,说明此位置上已经存在一个或多个数据,再次判断
        • 比较当前key与已经存在的数据的哈希值,如果当前key的哈希值与已存在数据的哈希值都不相同,则添加成功
        • 如果key与已存在 的某一个数据的哈希值相同,则通过equals方式比较
          • 如果equals返回false,说明两者不相同,则添加成功
          • 如果equals返回ture,说明两者相同,则将value替换旧值
  • 默认扩容为原来的2倍,并将原有的数据复制过来
  • JDK8相较于JDK7在底层实现方面的不同
    • new HashMap(); 没有创建长度16的数组
    • JDK8 底层是Node数组
    • 首次调用put()方法时,底层创建长度为16的数组
    • JDK7中只有数组+链表;JDK8中但数组中某一索引位置上的链表长度超过8 并且 当前数组长度超过 64 时,此时此索引位置上的链表转换成红黑树存储,以提升查找效率。

HashMap源码知识点

  • HashMap容量一定是2的n次幂
    • 原因在于在计算下标时的算法,如果不是2的n次幂会导致数组中某些位置永远不会被使用到
  • 扩容时机:当前数据量超过阈值时,进行扩容
    • 阈值 = 当前容量 * 加载因子
  • 加载因子
    • 如果加载因子设置得太大了,那么扩容时底层数组中存储的元素越多,导致查询效率越低
    • 如果加载因子设置得太小了,那么扩容时底层数组中光存储的元素越少,导致内存使用率越低
  • 扩容
    • 扩容时需要重新计算每个数据的下标,然后存储到对应的位置。这个操作比较消耗资源,所以最好设置默认值。