面试复盘(事成)

1.深拷贝和浅拷贝

浅拷贝:拷⻉对象和原始对象的引⽤类型引用同⼀个对象。

以下例子,Student对象里面有个Person对象,调用clone之后,克隆对象和原对象的Person引用的是同一个 对象,这就是浅拷贝。

public class Student implements Cloneable {
    private Person person;
    private String grade;
    //内部类
    static class Person {

        private String name;

        Person(String name){
            this.name = name;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }
    //重写克隆方法
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    public Person getPerson() {
        return person;
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        Student student = new Student();
        Person person = new Person("名字");
        student.person = person;
        Student Student2 = (Student)student.clone();
        person.setName("新名字");
        System.out.println(Student2.getPerson().getName());
    }
}
//输出:新名字

深拷贝:拷贝对象和原始对象的引用类型引用不同的对象。

深克隆的实现方法有很多,比如以下几个:

  1. 所有引用属性都实现克隆,整个对象就变成了深克隆。
  2. 使用 JDK 自带的字节流序列化和反序列化对象实现深克隆。
  3. 使用第三方工具实现深克隆,比如 Apache Commons Lang。
  4. 使用 JSON 工具,如 GSON、FastJSON、Jackson 序列化和反序列化对象实现深克隆。

以下例子,在clone函数中不仅调用了super.clone,而且调用Person对象的clone方法(Person也要实 现Cloneable接口并重写clone方法),从而实现了深拷贝。可以看到,拷贝对象的值不会受到原对象的 影响。

public class Student implements Cloneable {
    private Person person;
    private String grade;
    static class Person implements Cloneable{

        private String name;

        Person(String name){
            this.name = name;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
        @Override
        protected Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
    }
    //重写克隆方法
    @Override
    protected Object clone() throws CloneNotSupportedException {
        Student student = null;
        student = (Student) super.clone();
        student.person = (Person) person.clone();//拷贝Person对象
        return student;
    }

    public Person getPerson() {
        return person;
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        Student student = new Student();
        Person person = new Person("名字");
        student.person = person;
        Student Student2 = (Student)student.clone();
        person.setName("新名字");
        System.out.println(Student2.getPerson().getName());
    }
}
//输出:名字

 深克隆还可以通过利用序列化和反序列化(简单方便)

import java.io.*;

public class Student implements Serializable {
    private Person person;
    private String grade;
    static class Person implements Serializable {

        private String name;

        Person(String name){
            this.name = name;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }
    public Student myClone(){
        Student student = null;
        try {
            //将对象序列化到流里
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(os);
            oos.writeObject(this);
            //将流反序列化成对象
            ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(is);
            student = (Student) ois.readObject();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return student;
    }

    public Person getPerson() {
        return person;
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        Student student = new Student();
        Person person = new Person("名字");
        student.person = person;
        Student Student2 = (Student)student.myClone();
        person.setName("新名字");
        System.out.println(Student2.getPerson().getName());
    }
}

2.关于并发用过哪些

使用锁机制:锁机制是一种用于控制多个线程对共享资源进行访问的机制。在 Java 中,锁机制主要有两种:synchronized 关键字和 Lock 接口。synchronized 关键字是 Java 中最基本的锁机制,它可以用来修饰方法或代码块,以实现对共享资源的互斥访问。而 Lock 接口是 Java5 中新增的一种锁机制,它提供了比 synchronized 更强大、更灵活的锁定机制,例如可重入锁、读写锁等;

使用线程安全的容器:如 ConcurrentHashMap、Hashtable、Vector。需要注意的是,线程安全的容器底层通常也是使用锁机制实现的;

使用本地变量:线程本地变量是一种特殊的变量,它只能被同一个线程访问。在 Java 中,线程本地变量可以通过 ThreadLocal 类来实现。每个 ThreadLocal 对象都可以存储一个线程本地变量,而且每个线程都有自己的一份线程本地变量副本,因此不同的线程之间互不干扰。

volatile 关键字,用于保证多线程情况下共享变量的可见性。当一个变量被声明为 volatile 时,每个线程在访问该变量时都会立即刷新其本地内存(工作内存)中该变量的值,确保所有线程都能读到最新的值。并且使用 volatile 可以禁止指令重排序,这样就能有效的预防,因为指令优化(重排序)而导致的线程安全问题。volatile 有两个主要功能:保证内存可见性和禁止指令重排序。

synchronized 是 Java 中最基本的锁机制,使用它可以实现对共享资源的互斥访问。当一个线程访问被 synchronized 修饰的方法或代码块时,它会自动获取锁,其他线程只能排队等待该线程释放锁。

sping的声明式事务 @Transactional来开启线程。

当声明式事务 @Transactional 遇到以下场景时,事务会失效:

  1. 非 public 修饰的方法;
  2. timeout 设置过小;
  3. 代码中使用 try/catch 处理异常;
  4. 调用类内部 @Transactional 方法;
  5. 数据库不支持事务。

ThreadLocal 线程本地变量,每个线程都拥有一份该变量的独立副本,即使是在多线程环境下,每个线程也只能修改和访问自己的那份副本,从而避免了线程安全问题,实现了线程间的隔离。

3.值传递和引用传递

值传递(pass by value)是指在调用函数时将实际参数复制一份传递到函数中,而并不是将这个值直接传递给函数

引用传递(pass by reference)是指在调用函数时将实际参数的地址直接传递到函数中,而传递过来的地址还是与之前的地址指向同一个值,那么要是修改了这个参数,就会影响这个值的改变。

先是基础数据类型,应该没有疑问是值传递,在方法中修改值的大小不会影响原有值。

public class Test {
    public static void main(String[] args) {
        int value = 1;
        setValue(value);
        System.out.println("修改后值为:"+ value);
    }
    
    public static void setValue(int value) {
        value = 2;
        System.out.println("修改值为:" + value);
    }
}

//修改值为:2
//修改后值为:1

然后是引用数据类型,在方法中修改值的大小也不会影响原有值。

public class Test {
    public static void main(String[] args) {
        String value = "1";
        setValue(value);
        System.out.println("修改后值为:"+ value);
    }

    public static void setValue(String value) {
        value = "2";
        System.out.println("修改值为:" + value);
    }
}
//修改值为:2
//修改后值为:1

自定义类,在方法中修改值的大小也不会影响原有值。

public class Test {
    public static void main(String[] args) {
        Person value = new Person("1");
        setValue(value);
        System.out.println("修改后值为:"+ value.getName());
    }

    public static void setValue(Person value) {
        value = new Person("2");
        System.out.println("修改值为:" + value.getName());
    }
    static class Person {
        private String name;
        
        Person(String name){
            this.name = name;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
    }
}
//修改值为:2
//修改后值为:1

但是对于对象的值,

JVM虚拟机分为堆和栈,堆中存储对象实例和数组,它是所有线程共享的,对象在堆中通过引用进行访问。栈是为每个线程分配的内存区域,用于存储方法调用和局部变量等信息,方法的参数、局部变量和返回值都存储在栈帧中。

对于上述的例子,Person对象的实力存储在堆中,栈中有两块,一块是main方法栈里面有value引用,另一个块是一块是setValue方法栈里面也有value引用。

如果java是引用传递,那么两个栈用Value的引用应该指向的是堆中的同一块地址。

public class Test {
    public static void main(String[] args) {
        Person value = new Person("1");
        System.out.println("main中value修改后的引用地址"+value.toString());
        setValue(value);
        System.out.println("修改后值为:"+ value.getName());
        System.out.println("main中value修改后的引用地址"+value.toString());
        value = new Person("2");
        System.out.println("修改后值为:"+ value.getName());
        System.out.println("main中value修改后的引用地址2:"+value.toString());
    }

    public static void setValue(Person value) {
        System.out.println("setValue中value的引用地址"+value.toString());
        value = new Person("2");
        System.out.println("修改值为:" + value.getName());
        System.out.println("setValue中value修改后的引用地址"+value.toString());
    }
    static class Person {
        private String name;

        Person(String name){
            this.name = name;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
    }
}
//        main中value修改后的引用地址Test$Person@135fbaa4
//        setValue中value的引用地址Test$Person@135fbaa4
//        修改值为:2
//        setValue中value修改后的引用地址Test$Person@45ee12a7
//        修改后值为:1
//        main中value修改后的引用地址Test$Person@135fbaa4
//        修改后值为:2
//        main中value修改后的引用地址2:Test$Person@330bedb4

从上述代码可以看到setvalue中value的引用地址没修改前是和mian中value的引用地址是相同的。如果说是值传递 那么setvalue中value的引用地址没修改前是和mian中value的引用地址不应该相同吧,但如果是引用传递,那么在setvalue中对value的修改也应该会同步影响到main方法中的value啊,这又与事实不符。看到一种说法是对象类型的值是对象引用,那么就是值传递,但是传递的是引用地址,这就可以解释为什么setvalue中value的引用地址没修改前是和mian中value的引用地址是相同的。那么对于第二个疑问,也就是setvalue中value的引用地址相当于重新赋值了一个新的对象引用,也算合理。

对象类型的值是对象引用的理论也可以解释单独set属性值时,main中的value也会有影响。

public class Test {
    public static void main(String[] args) {
        Person value = new Person("1");
        System.out.println("main中value修改后的引用地址"+value.toString());
        setValue(value);
        System.out.println("修改后值为:"+ value.getName());
        System.out.println("main中value修改后的引用地址"+value.toString());
    }

    public static void setValue(Person value) {
        System.out.println("setValue中value的引用地址"+value.toString());
        value.setName("2");
        System.out.println("修改值为:" + value.getName());
        System.out.println("setValue中value修改后的引用地址"+value.toString());
    }
    static class Person {
        private String name;

        Person(String name){
            this.name = name;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
    }
}
//        main中value修改后的引用地址Test$Person@135fbaa4
//        setValue中value的引用地址Test$Person@135fbaa4
//        修改值为:2
//        setValue中value修改后的引用地址Test$Person@135fbaa4
//        修改后值为:2
//        main中value修改后的引用地址Test$Person@135fbaa4

4.函数式编程

Lambda 表达式是 Java 8 引入的一种简洁的表示匿名方法的方式,使用它可以用于替代某些匿名内部类对象,从而让程序更简洁,可读性更好。

Lambda 表达式主要用于执行函数式接口(Function Interface),即只有一个抽象方法的接口。常见的函数式接口包括 java.util.function 包下的 Predicate、Function、Consumer 等。

Lambda 底层运行原理如下:

  1. 在程序运行时,会在类中生成一个匿名内部类,匿名内部类会实现接口,并重写接口中的抽象方法。
  2. 类中会生成一个静态方法,静态方法中的代码就是 Lambda 表达式中的代码。
  3. 匿名内部类重写的抽象方法,会调用上一步的静态方法,从而实现 Lambda 代码的执行。

5.stream流

Java 8 API添加了一个新的抽象称为流Stream,可以让你以一种声明的方式处理数据。

Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。

  • stream() − 为集合创建串行流。

  • parallelStream() − 为集合创建并行流。

  • forEach() Stream 提供了新的方法 forEach来迭代流中的每个数据。

  • map() map 方法用于映射每个元素到对应的结果

  • filter() filter 方法用于通过设置的条件过滤出元素。

  • limit() limit 方法用于获取指定数量的流。

  • sorted() sorted 方法用于对流进行排序。

  • parallelStream() parallelStream 是流并行处理程序的代替方法。

  • Collectors() Collectors 类实现了很多归约操作,例如将流转换成集合和聚合元素。

6.过滤器和拦截器有什么区别

  • 出身不同:过滤器来自于 Servlet,而拦截器来自于 Spring 框架;

  • 触发时机不同:请求的执行顺序是:请求进入容器 > 进入过滤器 > 进入 Servlet > 进入拦截器 > 执行控制器(Controller),所以过滤器和拦截器的执行时机,是过滤器会先执行,然后才会执行拦截器,最后才会进入真正的要调用的方法;

  • 底层实现不同:过滤器是基于方法回调实现的,拦截器是基于动态代理(底层是反射)实现的;

  • 支持的项目类型不同:过滤器是 Servlet 规范中定义的,所以过滤器要依赖 Servlet 容器,它只能用在 Web 项目中;而拦截器是 Spring 中的一个组件,因此拦截器既可以用在 Web 项目中,同时还可以用在 Application 或 Swing 程序中;

  • 使用的场景不同:因为拦截器更接近业务系统,所以拦截器主要用来实现项目中的业务判断的,比如:登录判断、权限判断、日志记录等业务;而过滤器通常是用来实现通用功能过滤的,比如:敏感词过滤、字符集编码设置、响应数据压缩等功能。

7.怎么让自己的类可以像Lambda表达式一样创建

Lambda 底层运行原理如下:

  1. 在程序运行时,会在类中生成一个匿名内部类,匿名内部类会实现接口,并重写接口中的抽象方法。

  2. 类中会生成一个静态方法,静态方法中的代码就是 Lambda 表达式中的代码。

  3. 匿名内部类重写的抽象方法,会调用上一步的静态方法,从而实现 Lambda 代码的执行。

在自己定义的接口上加@FunctionalInterface注解, 它是 Java 8 引入的一个注解,它用于标记一个接口为函数式接口

@FunctionalInterface 注解的作用如下:

  1. 编译时检查:当一个接口被标记为 @FunctionalInterface 时,编译器会检查该接口是否只有一个抽象方法。如果不符合函数式接口的定义(即存在多个抽象方法),编译器会报错,提醒开发者修正。这为开发者提供了明确的编译时保障,确保所标记的接口确实符合函数式接口的要求。

  2. 代码明确性:即使不加 @FunctionalInterface 注解,只要接口符合函数式接口的定义,它仍然可以被视为函数式接口。但注解的存在增加了代码的明确性和可读性,使得其他开发者更容易理解该接口的设计意图。

  3. 支持 Lambda 表达式:函数式接口的主要目的是为了支持 Lambda 表达式。通过 Lambda 表达式,开发者可以以更简洁的方式实现函数式接口的抽象方法,从而减少模板代码,使代码更加简洁和易于理解。由于 Lambda 表达式本身不包含类型信息,Java 编译器需要一种机制来确定 Lambda 表达式对应的目标类型。函数式接口就扮演了这一角色——Lambda 表达式可以被赋值给任何兼容的函数式接口类型,编译器会依据接口的唯一抽象方法来推断 Lambda 表达式的参数类型和返回类型。

在 Java 标准库中,有许多使用 @FunctionalInterface 注解的接口,如 java.util.function 包下的 Function、Predicate、Consumer 等,这些接口都是函数式接口,广泛用于数据处理、过滤、转换等操作。此外,在 Spring Boot 框架中,也经常使用函数式接口来定义事件监听器、回调函数等。

7.1 可能是怎么实现链式调用?

  1. Setter 原生方式

  2. Lombok @Accessors 注解方式

  3. Lombok @Builder 注解方式

  4. Hutool GenericBuilder 方式

8.mybatis用过吗,了解mybatis的插件吗

MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生 SQL 查询,将接口和 Java 的实体类映射成数据库中的记录。

PageHelper分页插件。

9.mybatisplus用过吗,了解mybatisplus的插件吗

MyBatis-Plus可以简化CRUD操作,通过继承BaseMapper类,对于简单的CRUD操作可以直接使用,而不需要再自己写sql。

插件主体 | MyBatis-Plus (baomidou.com)

10.一张表 有两个字段值重复了,删除除了id最小的数据(笔试)

delete  from person where id not in (
select a.* from (
SELECT min(id) from person GROUP BY address,name
) a
)

11.递归实现斐波那契数列(笔试)

非递归实现。

import java.util.ArrayList;

public class Test {
    public static void main(String[] args) {
        String num = "100";
        math(num);
    }

    private static void math(String num) {
        ArrayList<Integer> arr = new ArrayList<Integer>();
        arr.add(1);
        arr.add(1);
        for (; Integer.valueOf(num)>arr.get(arr.size()-1)+arr.get(arr.size()-2) ;) {
            arr.add(arr.get(arr.size()-1)+arr.get(arr.size()-2));
        }
        System.out.println(arr.toString());
    }
}

递归实现。

import java.util.ArrayList;

public class Test {
    public static void main(String[] args) {
        String num = "100";
        ArrayList<Integer> arr = new ArrayList<Integer>();
        arr.add(1);
        arr.add(1);
        math(arr,num);
        System.out.println(arr.toString());
    }

    private static void math(ArrayList<Integer> arr,String num) {
        if(Integer.valueOf(num)>arr.get(arr.size()-1)+arr.get(arr.size()-2)){
            arr.add(arr.get(arr.size()-1)+arr.get(arr.size()-2));
            math(arr,num);
        }else {
            return;
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值