面试题-20220130

面试题地址

  1. JDK,JRE区别
  • JDK:java开发工具包,包含JRE和JVM。
  • JRE:java运行时环境
  • JVM:java虚拟机。正是因为有了JVM,java代码才有了一次编码,到处运行的特点。(即只要编写一次代码,可以在不同的操作系统中运行)

如果需要开发一个java程序,需要安装JDK。如果想要运行一个java程序。只安装JRE即可。

2.==和equals区别

==

  • 对于普通数据类型(四类八种),==比较的是值是否相同
  • 对于引用数据类型,==比较的是引用地址是否相同。

equals

  • 基本数据类型没有equals方法,基本数据类型的包装类有equals方法
  • equals本质也是==,String类重写了equals方法,比较的是内容是否相同。

3.两个对象的 hashCode()相同,则 equals()也一定为 true,对吗?

hashCode()方法返回的就是一个数值,从方法的名称上就可以看出,其目的是生成一个hash码。

在散列表中,hashCode()相等即两个键值对的哈希值相等,然而哈希值相等,并不一定能得出键值对相等。如:“通话”和“重地”的 hashCode() 相同,然而 equals() 则为 false。

当set集合(不可重复集合)要添加新的元素时,

  • 先调用这个元素的hashCode方法,一下子能定位到它应该放置的物理位置上。
  • 如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;
  • 如果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存,不相同就散列其它的地址。所以这里存在一个冲突解决的问题。这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次。

4.final 在 java 中有什么作用

  • 修饰类,表明该类不能被继承。
  • 修饰属性,表明该属性是一个常量,且必须为其赋值。
  • 修饰方法,表明该方法不能被子类重写。

5.String 属于基础的数据类型吗?

byte、int、short、long、double、float、char、boolean

  1. java 中操作字符串都有哪些类?它们之间有什么区别?
  • String:对字符串操作是每次都创建一个新的字符串,对新的字符串进行修改。
  • StringBuffer:线程安全,效率低。多线程使用。
  • StringBulider:线程不安全,效率高。单线程使用。
  1. String str="i"与 String str=new String("i")一样吗?
  • 不一样,前一个是在字符串常量池创建。后一个是堆内存里面创建一个对象。
  1. 将字符串反转
  • 使用 StringBuilder 或者 stringBuffer 的 reverse() 方法。
@Test
void myTest() {
    StringBuilder abc = new StringBuilder("abc");
    StringBuilder reverse = abc.reverse();
    System.out.println(reverse);
}
  1. String类中常用的方法
  • toCharArray() 转换为字符数组
  • equals() 比较字符串内容是否相同
  • toUpperCase() 全部转为大写
  • contains(Char s) 是否包含指定字符
  • substring(int index) 从指定索引开始截取,返回后面的字符
  • charAt(int index) 返回指定索引位置的字符。
  1. java 中 IO 流分为几种?
  • 按功能来分:输入流(input)、输出流(output)。
  • 按类型来分:字节流和字符流。
  • 字节流和字符流的区别是:字节流按 8 位传输以字节为单位输入输出数据,字符流按 16 位传输以字符为单位输入输出数据。
  1. List、Set、Map 之间的区别是什么?
  • List 集合。可以存相同值数据,有索引,有序。
  • Set 集合。不能存相同值数据,无索引,无序。
  • Map 集合。是以键值对形式存放数据的。键不能相同,值可以相同。无索引,无序。
  1. 如何决定使用 HashMap 还是 TreeMap?
  • 对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择。然而,假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选择。基于你的collection的大小,也许向HashMap中添加元素会更快,将map换为TreeMap进行有序key的遍历
  1. HashMap的原理及put()方法

JDK1.8及之后

  • HashMap的数据结构是数组+链表+红黑树。默认数组大小是64,扩容因子0.75。
  • 调用put方法时,先计算该对象的hash值,放入到对应的数组中。如果该数组已经存在值,将该对象以链表结构放在已存对象后面,如果链表长度大于8,链表转为红黑树结构。
  1. ArrayList 和 LinkedList 的区别是什么?

底层数据结构不同。

  • ArrayList 是数组结构。增删慢,查询快。
  • LinkedList 是链表结构。增删快,查询慢。
  1. 如何实现数组和 List 之间的转换?
  • 数组---->List:Arrays.asList()
  • List---->数组:list.toArray()
  1. 进程和线程

写好的一个程序保存在硬盘,当执行这个程序时,CPU将程序从硬盘读取到内存。

进程

  • 在内存中的可执行程序就称为进程。
  • 执行多个程序时,就会有多个进程被加载到内存中。这些进程都各自占用部分内存空间。
  • 进程是程序执行的完整单位
  • 每个进程内部都有自己的虚拟地址内存。

线程

  • 一个进程可以包含多个线程。
  • 线程是并行(CPU执行)的最小单位
  • 单核:CPU一次只能执行一个线程,通过分配时间片的方式运行线程。
  • 对于线程来说,等待CPU来执行(就绪)。CPU执行到该线程(运行)。CPU去执行其他线程(就绪)。该线程执行过程中需要访问硬盘数据(阻塞)。硬盘数据访问完成(就绪)。

  1. 并发和并行(重要)

并发

  • 是指两个或多个事件在同一时间间隔发生。
  • 如:单个处理器,先执行一个任务,然后快速切换执行下一个任务。

并行

  • 并行是指两个或者多个事件在同一时刻发生。
  • 如:多核处理器。两个CPU能同一时刻执行两个任务,不用通过分配时间片的方式运行。
  1. 异步和同步

同步

  • 上一个任务执行完毕后,才能执行下一个任务。

异步

  • 不同的任务不需要相互等待,可以同时运行。
  • 实现异步的方式就是使用多线程编程,在多核CPU:创建并启动多个线程,每个线程会分配到独立的CPU运行,实现真正的并行
  1. 创建线程的几种方法
  • 继承Thread类创建线程类
    • 定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
    • 创建Thread子类的实例,即创建了线程对象。
    • 调用线程对象的start()方法来启动该线程。
  • 通过Runnable接口创建线程类
    • 定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
    • 创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
    • Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;
    • 调用线程对象的start()方法来启动该线程。
  • 通过Callable和Future创建线程
    • 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
    • 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
    • Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
    • 使用FutureTask对象作为Thread对象的target创建并启动新线程。
    • 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
  1. 线程的几个状态

线程通常都有五种状态,创建、就绪、运行、阻塞和死亡。

  • 创建状态。在生成线程对象,并没有调用该对象的start方法,这是线程处于创建状态。
  • 就绪状态。当调用了线程对象的start方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。
  • 运行状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码。
  • 阻塞状态。线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend,wait等方法都可以导致线程阻塞。
  • 死亡状态。如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡。对于已经死亡的线程,无法再使用start方法令其进入就绪
  1. 在 java 程序中怎么保证多线程的运行安全?

线程安全在三个方面体现:

  • 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);
  • 可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile);
  • 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before原则)。
  1. 什么是死锁?

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去

22.说一下 synchronized 底层实现原理?

synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。

  • Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:
    • 普通同步方法,锁是当前实例对象
    • 静态同步方法,锁是当前类的class对象
    • 同步方法块,锁是括号里面的对象
  1. synchronized 和 Lock 有什么区别?
  • 首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
  • synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
  • synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
  • 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
  • synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可);
  • Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
  1. 什么是反射?

反射主要是指程序可以访问、检测和修改它本身状态或行为的一种能力
Java反射:

  • 在Java运行时环境中,对于任意一个类,能否知道这个类有哪些属性和方法?对于任意一个对象,能否调用它的任意一个方法
    Java反射机制主要提供了以下功能:
  • 在运行时判断任意一个对象所属的类。
  • 在运行时构造任意一个类的对象。
  • 在运行时判断任意一个类所具有的成员变量和方法。
  • 在运行时调用任意一个对象的方法。
  1. 什么是 java 序列化?什么情况下需要序列化?

简单说就是为了保存在内存中的各种对象的状态(也就是实例变量,不是方法),并且可以把保存的对象状态再读出来。
什么情况下需要序列化:

  • 当你想把的内存中的对象状态保存到一个文件中或者数据库中时候;
  • 当你想用套接字在网络上传送对象的时候;
  • 当你想通过RMI传输对象的时候;
  1. 动态代理是什么?有哪些应用?

动态代理:
当想要给实现了某个接口的类中的方法,加一些额外的处理。比如说加日志,加事务等。可以给这个类创建一个代理,故名思议就是创建一个新的类,这个类不仅包含原来类方法的功能,而且还在原来的基础上添加了额外处理的新类。这个代理类并不是定义好的,是动态生成的。具有解耦意义,灵活,扩展性强。
动态代理的应用:

  • Spring的AOP
  • 加事务
  • 加权限
  • 加日志
  1. 怎么实现动态代理?

首先必须定义一个接口,还要有一个InvocationHandler(将实现接口的类的对象传递给它)处理类。
再有一个工具类Proxy(习惯性将其称为代理类,因为调用他的newInstance()可以产生代理对象,其实他只是一个产生代理对象的工具类)。
利用到InvocationHandler,拼接代理类源码,将其编译生成代理类的二进制码,利用加载器加载,并将其实例化产生代理对象,最后返回。

  1. throw 和 throws 的区别?
  • throws是用来声明一个方法可能抛出的所有异常信息,throws是将异常声明但是不处理,而是将异常往上传,谁调用我就交给谁处理。而throw则是指抛出的一个具体的异常类型。
  1. final、finally、finalize 有什么区别?
  • final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。
  • finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。
  • finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调用,当我们调用System的gc()方法的时候,由垃圾回收器调用finalize(),回收垃圾
  1. 单例模式

简单点说,就是一个应用程序中,某个类的实例对象只有一个,你没有办法去new,因为构造器是被private修饰的,一般通过getInstance()的方法来获取它们的实例。

// 懒汉式
public class Singleton {

private static Singleton singleton;

private Singleton() {
}

public static Singleton getInstance() {
 if (singleton == null) {
  singleton = new Singleton();
 }
 return singleton;
}
}
//饿汉式
public class Singleton {  
   private static Singleton instance = new Singleton();  
   private Singleton (){}  
   public static Singleton getInstance() {  
   return instance;  
   }  
}
  1. spring

简单来说,Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架。

2.轻量  

  • 从大小与开销两方面而言Spring都是轻量的。完整的Spring框架可以在一个大小只有1MB多的JAR文件里发布。并且Spring所需的处理开销也是微不足道的。此外,Spring是非侵入式的:典型地,Spring应用中的对象不依赖于Spring的特定类。
    3.控制反转  
  • Spring通过一种称作控制反转(IoC)的技术促进了松耦合。当应用了IoC,一个对象依赖的其它对象会通过被动的方式传递进来,而不是这个对象自己创建或者查找依赖对象。你可以认为IoC与JNDI相反——不是对象从容器中查找依赖,而是容器在对象初始化时不等对象请求就主动将依赖传递给它。
    4.面向切面  
  • Spring提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务(例如审计(auditing)和事务(transaction)管理)进行内聚性的开发。应用对象只实现它们应该做的——完成业务逻辑——仅此而已。它们并不负责(甚至是意识)其它的系统级关注点,例如日志或事务支持。
    5.容器
  • Spring包含并管理应用对象的配置和生命周期,在这个意义上它是一种容器,你可以配置你的每个bean如何被创建——基于一个可配置原型(prototype),你的bean可以创建一个单独的实例或者每次需要时都生成一个新的实例——以及它们是如何相互关联的。然而,Spring不应该被混同于传统的重量级的EJB容器,它们经常是庞大与笨重的,难以使用。
    6.框架
  • Spring可以将简单的组件配置、组合成为复杂的应用。在Spring中,应用对象被声明式地组合,典型地是在一个XML文件里。Spring也提供了很多基础功能(事务管理、持久化框架集成等等),将应用逻辑的开发留给了你。
  • 所有Spring的这些特征使你能够编写更干净、更可管理、并且更易于测试的代码。它们也为Spring中的各种模块提供了基础支持。
  1. Spring是线程安全的吗

Spring默认使用的是单例bean模式,大部分情况是线程安全的。因为使用的是ThreadLocal,每次操作变量变量时,都会给该变量创建一个副本。所以每个线程独立拥有自己的副本,不会影响到其他变量。
当操作全局变量或静态变量时就不是线程安全的了。

  1. 如何保证Spring全局变量线程安全
  1. 将全局变量修改为局部变量
  2. 使用ThreadLocal来定义全局变量
private ThreadLocal<String> contentTL = new ThreadLocal<String>();
//为该全局变量赋值时
this.contentTL.set(content);  
//获取全局变量时
this.contentTL.get(content);
  1. mybatis 中 #{}和 ${}的区别是什么?
  • #{}是预编译处理,${}是字符串替换;
  • Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值;
  • Mybatis在处理${}时,就是把${}替换成变量的值;
  • 使用#{}可以有效的防止SQL注入,提高系统安全性。

35.怎么防止表单重复提交

    1. 表单点击提交后禁用提交按钮
    1. 前端传入唯一requestId,后端根据请求id判断是否重复提交
    1. 通过session来判断是否是同一请求.

36.如何删除集合中指定元素

ArrayList集合底层是数组,有索引,如果使用for循环来遍历再进行删除的话,将前一个元素删除,后面的所有元素所有都会前移。

List<String> list=new ArrayList<>();
 
list.add("b");
list.add("b");
list.add("e");
list.add("c");

for(int i=0; i<list.size();i++){
  if("b".equals(list.get(i))){
    list.remove(i);    //b,e,c 只删除第一个b
  }
}

解决方法;使用迭代器进行删除

while(it.hasNext()) {
  if("b".equals(it.next())) {
  it.remove();
}

37.如何ArrayList去重

1.使用LinkedHashSet进行去重

 public static void main(String[] args) {
        ArrayList<Integer> numbersList = new ArrayList<>(Arrays.asList(1, 1, 2, 3, 3, 3, 4, 5, 6, 6, 6, 7, 8));
        System.out.println(numbersList);
        LinkedHashSet<Integer> hashSet = new LinkedHashSet<>(numbersList);
        ArrayList<Integer> listWithoutDuplicates = new ArrayList<>(hashSet);
        System.out.println(listWithoutDuplicates);
    }
  1. 使用流对集合中的distinct()方法进行过滤
public class ArrayListExample {
    public static void main(String[] args) {
        ArrayList<Integer> numbersList = new ArrayList<>(Arrays.asList(1, 1, 2, 3, 3, 3, 4, 5, 6, 6, 6, 7, 8));
        System.out.println(numbersList);
        List<Integer> listWithoutDuplicates = numbersList.stream().distinct().collect(Collectors.toList());
        System.out.println(listWithoutDuplicates);
    }
}

38.高并发情况下如何使用List集合

  1. 可以使用Vector集合,该集合是线程安全的,但是效率较低。
  2. 可以使用Collections集合包装类中的一些方法创建线程安全的集合。
List<String> list = new ArrayList<>();
//使用collections集合工具类中的方法
List<String> list2 = Collections.synchronizedList(new ArrayList<String>());
  1. 说下线程池的使用情况

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?

创建线程3中方式

  • 继承Thread类,重写run方法
  • 实现Runnable接口,重新run方法,无返回值。
  • 实现Callable接口,重新call方法,有返回值。

但是我们工作中一般不这样来创建线程。原因:虽然在 Java 语言中创建线程看上去就像创建一个对象一样简单,只需要 new Thread() 就可以了,但实际上创建线程远不是创建一个对象那么简单。创建对象,仅仅是在 JVM 的堆里分配一块内存而已;而创建一个线程,却需要调用操作系统内核的 API,然后操作系统要为线程分配一系列的资源,这个成本就很高了,所以线程是一个重量级的对象,应该避免频繁创建和销毁。

posted @ 2022-02-16 10:13  初夏那片海  阅读(63)  评论(0)    收藏  举报