您的当前位置:首页>全部文章>文章详情

大厂真题之阿里云-Java实习生,面试记录整理

发表于:2024-01-14 23:01:05浏览:97次TAG: #Java知识 #Java面试题

1、List 和 Set 的区别

List , Set 都是继承自 Collection 接口

List 特点:元素有放入顺序,元素可重复 

Set 特点:元素无放入顺序,元素不可重复,重复元素会覆盖掉,(元素虽然无放入顺序,但是元素在set中的位置是有该元素的 HashCode 决定的,其位置其实是固定的,加入Set 的 Object 必须定义 equals ()方法 ,另外list支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。)

Set和List对比 

Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。

List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变

2、 讲一下常见编码方式?

编码的意义:计算机中存储的最小单元是一个字节即 8bit, 所能表示的字符范围是 255 个,而人类要表示的符号太多, 无法用一个字节来完全表示, 固需要将符号编码, 将各种语言翻译成计算机能懂的语言。

1)ASCII 码:总共 128 个, 用一个字节的低 7 位表示, 0〜31 控制字符如换回车删除等;32~126是打印字符, 可通过键盘输入并显示出来;

2) ISO-8859-1,用来扩展 ASCII 编码, 256 个字符, 涵盖了大多数西欧语言字符。

3) GB2312:双字节编码, 总编码范围是 A1-A7,A1-A9 是符号区, 包含 682 个字符, B0-B7 是汉字区, 包含 6763 个汉字;

4) GBK 为了扩展 GB2312,加入了更多的汉字, 编码范围是 8140~FEFE, 有 23940 个码位, 能表示 21003 个汉字。

5) UTF-16: ISO 试图想创建一个全新的超语言字典, 世界上所有语言都可通过这本字典Unicode 来相互翻译, 而 UTF-16 定义了 Unicode 字符在计算机中存取方法, 用两个字节来表示 Unicode 转化格式。不论什么字符都可用两字节表示, 即 16bit, 固叫 UTF-16。

6) UTF-8:UTF-16 统一采用两字节表示一个字符, 但有些字符只用一个字节就可表示, 浪费存储空间, 而 UTF-8 采用一种变长技术, 每个编码区域有不同的字码长度。不同类型的字 符 可 以 由 1~6 个 字 节 组成。

3、HashMap 是线程安全的吗,为什么不是线程安全的(最好画图说明多线程环境下不安全)?

不是线程安全的;

如果有两个线程AB,都进行插入数据,刚好这两条不同的数据经过哈希计算后得到的哈希码是一样的,且该位置还没有其他的数据。

所以这两个线程都会进入我在上面标记为1的代码中。假设一种情况,线程A通过if判断,该位置没有哈希冲突,进入了if语句,还没有进行数据插入,这时候 CPU 就把资源让给了线程B,线程A停在了if语句里面,线程B判断该位置没有哈希冲突(线程A的数据还没插入),也进入了if语句,线程B执行完后,轮到线程A执行,现在线程A直接在该位置插入而不用再判断。

这时候,你会发现线程A把线程B插入的数据给覆盖了。发生了线程不安全情况。本来在 HashMap 中,发生哈希冲突是可以用链表法或者红黑树来解决的,但是在多线程中,可能就直接给覆盖了。

上面所说的是一个图来解释可能更加直观。如下面所示,两个线程在同一个位置添加数据,后面添加的数据就覆盖住了前面添加的。

上面所说的是一个图来解释可能更加直观。如下面所示,两个线程在同一个位置添加数据,后面添加的数据就覆盖住了前面添加的。

如果上述插入是插入到链表上,如两个线程都在遍历到最后一个节点,都要在最后添加一个数据,那么后面添加数据的线程就会把前面添加的数据给覆盖住。则在扩容的时候也可能会导致数据不一致,因为扩容是从一个数组拷贝到另外一个数组。

4、HashMap 的扩容过程

当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于阈值(知道这个阈字怎么念吗?不念 fa 值,念 yu 值 第四声) --- 即当前数组的长度乘以加载因子的值的时候,就要自动扩容啦。
扩容( resize )就是重新计算容量,向 HashMap 对象里不停的添加元素,而 HashMap 对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。当然 Java 里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组,就像我们用一个小桶装水,如果想装更多的水,就得换大水桶。
HashMap hashMap=new HashMap(cap);
cap =3, hashMap 的容量为4
cap =4, hashMap 的容量为4
cap =5, hashMap 的容量为8
cap =9, hashMap 的容量为16
如果 cap 是2的n次方,则容量为 cap ,否则为大于 cap 的第一个2的n次方的数。

5、HashMap 1.7 与 1.8 的 区别,说明 1.8 做了哪些优化,如何优化的?

HashMap结构图

在 JDK1.7 及之前的版本中, HashMap 又叫散列链表:基于一个数组以及多个链表的实现,hash值冲突的时候,就将对应节点以链表的形式存储。

JDK1.8 中,当同一个hash值( Table 上元素)的链表节点数不小于8时,将不再以单链表的形式存储了,会被调整成一颗红黑树。这就是 JDK7 与 JDK8 中 HashMap 实现的最大区别。

其下基于 JDK1.7.0_80 与 JDK1.8.0_66 做的分析

JDK1.7

使用一个 Entry 数组来存储数据,用key的 hashcode 取模来决定key会被放到数组里的位置,如果 hashcode 相同,或者 hashcode 取模后的结果相同( hash collision ),那么这些 key 会被定位到 Entry 数组的同一个格子里,这些 key 会形成一个链表。

在 hashcode 特别差的情况下,比方说所有key的 hashcode 都相同,这个链表可能会很长,那么 put/get 操作都可能需要遍历这个链表,也就是说时间复杂度在最差情况下会退化到 O(n)。

JDK1.8

使用一个 Node 数组来存储数据,但这个 Node 可能是链表结构,也可能是红黑树结构

如果插入的 key 的 hashcode 相同,那么这些key也会被定位到 Node 数组的同一个格子里。

如果同一个格子里的key不超过8个,使用链表结构存储。

如果超过了8个,那么会调用 treeifyBin 函数,将链表转换为红黑树。

那么即使 hashcode 完全相同,由于红黑树的特点,查找某个特定元素,也只需要O(log n)的开销也就是说put/get的操作的时间复杂度最差只有 O(log n)。

听起来挺不错,但是真正想要利用 JDK1.8 的好处,有一个限制:key的对象,必须正确的实现了 Compare 接口

如果没有实现 Compare 接口,或者实现得不正确(比方说所有 Compare 方法都返回0)那 JDK1.8 的 HashMap 其实还是慢于 JDK1.7 

简单的测试数据如下:

向 HashMap 中 put/get 1w 条 hashcode 相同的对象

JDK1.7: put 0.26s , get 0.55s

JDK1.8 (未实现 Compare 接口):put 0.92s , get 2.1s

但是如果正确的实现了 Compare 接口,那么 JDK1.8 中的 HashMap 的性能有巨大提升,这次 put/get 100Whashcode 相同的对象

JDK1.8 (正确实现 Compare 接口,):put/get 大概开销都在320 ms 左右

6、 谈谈你对解析与分派的认识。

解析指方法在运行前, 即编译期间就可知的, 有一个确定的版本, 运行期间也不会改变。解析是静态的, 在类加载的解析阶段就可将符号引用转变成直接引用。

分派可分为静态分派和动态分派, 重载属于静态分派, 覆盖属于动态分派。静态分派是指在重载时通过参数的静态类型而非实际类型作为判断依据, 在编译阶段, 编译器可根据参数的静态类型决定使用哪一个重载版本。动态分派则需要根据实际类型来调用相应的方法。

7、Java获取反射的三种方法

1.通过new对象实现反射机制 2.通过路径实现反射机制 3.通过类名实现反射机制
public class Student {
private int id;
String name;
protected boolean sex;
public float score;
}
public class Get {
//获取反射机制三种方式
public static void main(String[] args) throws ClassNotFoundException {
//方式一(通过建立对象)
Student stu = new Student();
Class classobj1 = stu.getClass();
System.out.println(classobj1.getName());
//方式二(所在通过路径-相对路径)
Class classobj2 = Class.forName("fanshe.Student");
System.out.println(classobj2.getName());
//方式三(通过类名)
Class classobj3 = Student.class;
System.out.println(classobj3.getName());
}
}

8、Java反射机制

Java 反射机制是在运行状态中,对于任意一个类,都能够获得这个类的所有属性和方法,对于任意一个对象都能够调用它的任意一个属性和方法。这种在运行时动态的获取信息以及动态调用对象的方法的功能称为 Java 的反射机制。
Class 类与 java.lang.reflect 类库一起对反射的概念进行了支持,该类库包含了 Field,Method,Constructor 类 (每个类都实现了 Member 接口)。这些类型的对象时由 JVM 在运行时创建的,用以表示未知类里对应的成员。
这样你就可以使用 Constructor 创建新的对象,用 get() 和 set() 方法读取和修改与 Field 对象关联的字段,用invoke() 方法调用与 Method 对象关联的方法。另外,还可以调用 getFields() getMethods() 和 getConstructors() 等很便利的方法,以返回表示字段,方法,以及构造器的对象的数组。这样匿名对象的信息就能在运行时被完全确定下来,而在编译时不需要知道任何事情。
import java.lang.reflect.Constructor;

public class ReflectTest {
public static void main(String[] args) throws Exception {
Class clazz = null;
clazz = Class.forName("com.jas.reflect.Fruit");
Constructor<Fruit> constructor1 = clazz.getConstructor();
Constructor<Fruit> constructor2 = clazz.getConstructor(String.class);

Fruit fruit1 = constructor1.newInstance();
Fruit fruit2 = constructor2.newInstance("Apple");
}
}
class Fruit{
public Fruit(){
System.out.println("无参构造器 Run...........");
}
public Fruit(String type){
System.out.println("有参构造器 Run..........." + type);
}
}
运行结果:无参构造器 Run……….. 有参构造器 Run………..Apple

9、Arrays.sort 和 Collections.sort 实现原理和区别?

Collection和Collections区别
 
java.util.Collection 是一个集合接口。它提供了对集合对象进行基本操作的通用接口方法。java.util.Collections 是针对集合类的一个帮助类,他提供一系列静态方法实现对各种集合的搜索、排序、线程安全等操作。
然后还有混排(Shuffling)、反转(Reverse)、替换所有的元素(fill)、拷贝(copy)、返回Collections中最小元素(min)、返回Collections中最大元素(max)、返回指定源列表中最后一次出现指定目标列表的起始位置( lastIndexOfSubList )、返回指定源列表中第一次出现指定目标列表的起始位置( IndexOfSubList )、根据指定的距离循环移动指定列表中的元素(Rotate);
事实上Collections.sort方法底层就是调用的array.sort方法,
public static void sort(Object[] a) {
if (LegacyMergeSort.userRequested)
legacyMergeSort(a);
else
ComparableTimSort.sort(a, 0, a.length, null, 0, 0);
}
//void java.util.ComparableTimSort.sort()
static void sort(Object[] a, int lo, int hi, Object[] work,int workBase, int workLen){
assert a != null && lo >= 0 && lo <= hi && hi <= a.length;
int nRemaining = hi - lo;

if (nRemaining < 2)
return; // Arrays of size 0 and 1 are always sorted

// If array is small, do a "mini-TimSort" with no merges
if (nRemaining < MIN_MERGE) {
int initRunLen = countRunAndMakeAscending(a, lo, hi);
binarySort(a, lo, hi, lo + initRunLen);
return;
}
}
legacyMergeSort (a): 归并排序 ComparableTimSort.sort() :Timsort 排序 Timsort 排序是结合了合并排序(merge sort)和插入排序(insertion sort)而得出的排序算法
Timsort的核心过程:
TimSort 算法为了减少对升序部分的回溯和对降序部分的性能倒退,将输入按其升序和降序特点进行了分 区。排序的输入的单位不是一个个单独的数字,而是一个个的块-分区。其中每一个分区叫一个run。针对这些 run 序列,每次拿一个 run出来按规则进行合并。每次合并会将两个 run合并成一个 run。合并的结果保存到栈中。合并直到消耗掉所有的 run,这时将栈上剩余的 run合并到只剩一个 run 为止。这时这个仅剩的run 便是排好序的结果
综上述过程,Timsort算法的过程包括 :
(0)如何数组长度小于某个值,直接用二分插入排序算法 
(1)找到各个run,并入栈 
(2)按规则合并run

10、LinkedHashMap 的应用

基于 LinkedHashMap 的访问顺序的特点,可构造一个 LRULeast Recently Used) 最近最少使用简单缓存。也有一些开源的缓存产品如 ehcache 的淘汰策略( LRU )就是在 LinkedHashMap 上扩展的。

11、 说说你对 Java 注解的理解

注解是通过@interface 关键字来进行定义的, 形式和接口差不多, 只是前面多了一个@

public @interface TestAnnotation {

使用时@TestAnnotation 来引用, 要使注解能正常工作, 还需要使用元注解, 它是可以注解到注解上的注解。

元标签有@Retention @Documented @Target @Inherited @Repeatable 五种

@Retention 说明注解的存活时间, 取值有 RetentionPolicy.SOURCE 注解只在源码阶段保留,在编译器进行编译时被丢弃;RetentionPolicy.CLASS 注解只保留到编译进行的时候, 并不会被加载到 JVM 中。RetentionPolicy.RUNTIME 可以留到程序运行的时候, 它会被加载进入到 JVM中, 所以在程序运行时可以获取到它们。

@Documented 注解中的元素包含到 javadoc 中去

@Target 限 定 注 解 的 应 用 场 景 , ElementType.FIELD 给 属 性 进 行 注 解 ;ElementType.LOCAL_VARIABLE 可以给局部变量进行注解;ElementType.METHOD 可以给方法进行注解;ElementType.PACKAGE 可以给一个包进行注解 ElementType.TYPE 可以给一个类型进行注解, 如类、 接口、 枚举

@Inherited 若一个超类被@Inherited 注解过的注解进行注解, 它的子类没有被任何注解应用的话, 该子类就可继承超类的注解;

注解的作用:

1) 提供信息给编译器:编译器可利用注解来探测错误和警告信息

2) 编译阶段:软件工具可以利用注解信息来生成代码、 html 文档或做其它相应处理;

3) 运行阶段:程序运行时可利用注解提取代码

注解是通过反射获取的, 可以通过 Class 对象的 isAnnotationPresent()方法判断它是否应用了某个注解, 再通过 getAnnotation()方法获取 Annotation 对象

12、wait 和 sleep 的区别

源码如下
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
public class Thread implements Runnable {public static native void sleep(long millis) throws InterruptedException;public static void sleep(long millis, int nanos) throws InterruptedException {
if (millis < 0) {throw new IllegalArgumentException("timeout value is negative"); }
if (nanos < 0 || nanos > 999999) {throw new IllegalArgumentException( "nanosecond timeout value out of range"); }
if (nanos >= 500000 || (nanos != 0 && millis == 0)) { millis++; } sleep(millis); }//...}

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
public class Object {public final native void wait(long timeout) throws InterruptedException;public final void wait(long timeout, int nanos) throws InterruptedException {if (timeout < 0) {throw new IllegalArgumentException("timeout value is negative");    }if (nanos < 0 || nanos > 999999) {throw new IllegalArgumentException( "nanosecond timeout value out of range");    }if (nanos > 0) {       timeout++;    }    wait(timeout);  }//...}

 

  • 1、 sleep 来自 Thread 类,和 wait 来自 Object 类。
  • 2、最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
  • 3、wait,notify和 notifyAll 只能在同步控制方法或者同步控制块里面使用,而 sleep 可以在任何地方使用(使用范围)
  • 4、 sleep 必须捕获异常,而 wait , notify 和 notifyAll 不需要捕获异常
(1) sleep 方法属于 Thread 类中方法,表示让一个线程进入睡眠状态,等待一定的时间之后,自动醒来进入到可运行状态,不会马上进入运行状态,因为线程调度机制恢复线程的运行也需要时间,一个线程对象调用了 sleep方法之后,并不会释放他所持有的所有对象锁,所以也就不会影响其他进程对象的运行。
 
但在 sleep 的过程中过程中有可能被其他对象调用它的 interrupt() ,产生 InterruptedException 异常,如果你的程序不捕获这个异常,线程就会异常终止,进入 TERMINATED状态,如果你的程序捕获了这个异常,那么程序就会继续执行catch语句块(可能还有 finally 语句块)以及以后的代码。
注意 sleep() 方法是一个静态方法,也就是说他只对当前对象有效,通过 t. sleep() 让t对象进入 sleep ,这样的做法是错误的,它只会是使当前线程被 sleep 而不是 t 线程。
(2) wait 属于 Object 的成员方法,一旦一个对象调用了wait方法,必须要采用 notify() 和 notifyAll() 方法唤醒该进程;如果线程拥有某个或某些对象的同步锁,那么在调用了 wait() 后,这个线程就会释放它持有的所有同步资源,而不限于这个被调用了 wait() 方法的对象。wait() 方法也同样会在 wait 的过程中有可能被其他对象调用 interrupt() 方法而产生 。
 

13、 Java 中 String 的了解

1) String 类是 final 型, 固 String 类不能被继承, 它的成员方法也都默认为 final 方法。

String对象一旦创建就固定不变了, 对 String 对象的任何改变都不影响到原对象, 相关的任何改变操作都会生成新的 String 对象。

2) String 类是通过 char 数组来保存字符串的, String 对 equals 方法进行了重定, 比较的是值相等。

String a = "test"; String b = "test"; String c = new String("test");

a、 b 和字面上的 test 都是指向 JVM 字符串常量池中的"test"对象, 他们指向同一个对象。

而new 关键字一定会产生一个对象 test, 该对象存储在堆中。

所以 new String("test")产生了两个对象, 保存在栈中的 c 和保存在堆中的 test。

而在 java 中根本就不存在两个完全一模一样的字符串对象, 故在堆中的 test 应该是引用字符串常量池中的 test。

例:

String str1 = "abc"; //栈中开辟一块空间存放引用 str1, str1 指向池中 String 常量"abc" 

String str2 = "def"; //栈中开辟一块空间存放引用 str2, str2 指向池中 String 常量"def" 

String str3 = str1 + str2;//栈中开辟一块空间存放引用 str3

//str1+str2 通过 StringBuilder 的最后一步 toString()方法返回一个新的 String 对象"abcdef" 

//会在堆中开辟一块空间存放此对象, 引用 str3指向堆中的(str1+str2)所返回的新 String对象。

System.out.println(str3 == "abcdef");//返回 false

因为 str3 指向堆中的"abcdef"对象, 而"abcdef"是字符池中的对象, 所以结果为 false。 JVM对 String str="abc"对象放在常量池是在编译时做的, 而 String str3=str1+str2 是在运行时才知道的, new 对象也是在运行时才做的。

更多文章,请关注公众号【程序员李木子】,有免费的电子书哦