spice and wolfspice and wolf Be the One you wanna Be

java基础面试题

单例模式有哪几种实现方式?

  • 饿汉式。
    • 优点:线程安全,因为类的实例引用是静态的且赋了初始值,所以Java虚拟机会在类加载的初始化阶段保证这个静态实例对象创建的线程安全性。
    • 缺点:
      • 没有延迟初始化,如果类加载和创建实例耗时较长,如果在应用启动时就需要进行该类的类加载,并且希望应用能快启动时,可能并不适用,具体根据实际情况判断;
      • 类加载后如果一直没有被使用到,会持续占据内存空间。
public class Singleton {
    private static Singleton instance = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return instance;
    }
}
  • 懒汉式。
    • 优点:延迟初始化,只在需要时才会创建类实例,节省了内存空间。
    • 缺点:懒汉模式需要自行保证实例创建过程中的线程安全性。
// synchronized线程同步方式
public class SynchronizedSingleton {
    private static SynchronizedSingleton instance;

    private SynchronizedSingleton() {}

    public synchronized static SynchronizedSingleton getInstance() {
        if (instance == null) {
            instance = new SynchronizedSingleton();
        }
        return instance;
    }
}
// 双重检查锁线程同步方式
public class DoubleCheckLockSingleton {
    private static volatile DoubleCheckLockSingleton instance;

    private DoubleCheckLockSingleton() {}

    public static DoubleCheckLockSingleton getInstance() {
        if (instance == null) {
            synchronized (DoubleCheckLockSingleton.class) {
                if (instance == null) {
                    instance = new DoubleCheckLockSingleton();
                }
            }
        }
        return instance;
    }
}
  • 静态内部类。静态内部类不仅实现了延迟初始化还保证了线程安全。
public class StaticInnerClassSingleton {

    private static class StaticInnerClass {
        private static final StaticInnerClassSingleton instance = new StaticInnerClassSingleton();
    }

    private StaticInnerClassSingleton() {}

    public StaticInnerClassSingleton getInstance() {
        return StaticInnerClass.instance;
    }
}
  • 枚举方式。枚举方式的单例模式,不但线程安全,且自动支持序列化机制,不光如此,还能防止通过反射机制拿到私有构造方法进行实例化,是现有单例模式中的最优解。
public class Singleton {
}
public enum SingletonEnum {
    INSTANCE;
    private Singleton instance;
    private SingletonEnum() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        instance = new Singleton();
    }
    public Singleton getInstance() {
        return instance;
    }

    public static void main(String[] args) {
        for (int i = 1; i <= 50; i++) {
            new Thread(() -> {
                Singleton ins = SingletonEnum.INSTANCE.getInstance();
                System.out.println(Thread.currentThread().getName() + "\t:\t" + ins);
            }, "Thread" + String.valueOf(i)).start();
        }
    }
}

StringBuffer和StringBuilder的区别?

  • 共同点。有共同父类AbstractStringBuilder,都实现了Serializable和CharSequence接口,使用的方法基本以父类为蓝本。
  • 不同点。
    • 加入版本不同。StringBuffer在1.0版本加入到JDK中,StringBuilder在1.5版本加入到JDK中。
    • 性能。单线程情况下,StringBuilder效率高于StringBuffer。
    • 优化点。StringBuffer有一个字符数组成员变量toStringCache,在字符串不变的情况下,用来给new String(char[], boolean)作为对象创建的参数,避免频繁的字符数组深拷贝操作。
    • 线程安全性。StringBuffer的方法上都有synchronized关键字修饰,StringBuilder方法没有用synchronized关键字修饰,append(String str)方法前者能有效保障线程安全,后者则可能报ArrayIndexOutOfBoundsException异常:
public class StringBufferAndStringBuilder {
    public static void stringBufferConnect(StringBuffer stringBuffer, int s) {
        try {
            TimeUnit.MICROSECONDS.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        for (int i = 0; i < 100; i++) {
            stringBuffer.append(String.valueOf(s));
        }
    }

    public static void stringBuilderConnect(StringBuilder stringBuilder, int s) {
        try {
            TimeUnit.MICROSECONDS.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        for (int i = 0; i < 100; i++) {
            stringBuilder.append(String.valueOf(s));
        }
    }

    public static void main(String[] args) throws InterruptedException {
        StringBuffer stringBuffer = new StringBuffer();
        StringBuilder stringBuilder = new StringBuilder();
        List<Thread> threadList = new ArrayList<>();
        for (int i = 0; i < 200; i++) {
            int finalI = i;
            Thread tempThread = new Thread(() -> {
                stringBufferConnect(stringBuffer, finalI);
                stringBuilderConnect(stringBuilder, finalI);
            }, String.valueOf(i));
            threadList.add(tempThread);
            tempThread.start();
        }

        for (Thread thread : threadList) {
            thread.join();
        }

        System.out.println(stringBuffer.toString());
        System.out.println(stringBuffer.length());

        System.out.println(stringBuilder.toString());
        System.out.println(stringBuilder.length());
    }
}

这里StringBuffer只能保证最后字符长度一致,因为是循环append,所以并不能保证字符串拼接的顺序。StringBuffer类并不是完全线程安全的,像append(char[] str, int offset, int len)方法,数组引用破坏了锁的对内部对象的同步控制,导致外部对象依然能对数组进行操作,处理不慎依然会报索引溢出异常。

  • 应用场景。
    • 单线程场景下StringBuilder优于StringBuffer。
    • 多线程并发量少的场景下可以使用StringBuffer,逻辑实现更方便。
    • 多线程高并发场景使用StringBuilder并自行编写并发逻辑的性能要优于StringBuffer,因为StringBuffer方法中的synchronized锁太重了。

==和equals的区别

  • ==
    • 相同基本数据类型比较时,是基本数据类型值的比较。
    • 相同的引用类型比较时,是引用对象地址的比较,即是否是指向同一块内存地址。
  • equals
    • 默认情况(没有重写equals方法)下是比较两对象的地址,相当于==;基本类型的包装类除外,它们会进行拆箱并比较包装的基本数据类型的值;String类重写了equals方法,会先比较两个String对象地址是否相同,如果不相同,再比较字符串值是否完全一样,一样则返回true,否则返回false。
    • 重写equals方法后,会根据重写逻辑进行结果返回。
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private String name;
    private Integer age;
}
public class EqualsDemo {
    public static void main(String[] args) {
        String s1 = "11";
        String s2 = "11";
        System.out.println(s1 == s2);  // true
        System.out.println(s1.equals(s2));  // true

        Integer i1 = 1;
        Integer i2 = 1;
        System.out.println(i1 == i2);  // true
        System.out.println(i1.equals(i2));  // true

        User u1 = new User(s1, i1);
        User u2 = new User(s2, i2);
        System.out.println(u1 == u2); // false
        System.out.println(u1.equals(u2)); // 为重新equals则为false,重写equals后视逻辑而定

        int[] a = new int[9];
        int[] b = a;
        System.out.println(a == b);
    }
}

为什么重写equals就一定要重写hashcode方法

先来说说equals方法的作用,equals方法本意是为了比较两个对象逻辑上是否相同(相等),比如分别new两个值为”11″的String对象,虽然其是不同的两个内存对象内存地址不同,但是逻辑上这两个对象是相同的,当用equals比较时,返回为true。
hashcode在hashmap中和equals起到的作用相同,是用来判断hashmap中两个对象是否逻辑相等。hashcode方法在对象添加到map等数据结构时会调用,用来计算hash值,并根据hash值插入到对应数据结构中,如果equals判断但是hash值不同,就会逻辑上相等的数据操作的是不同的value值。举例来说,我们现在以String为例,如果名字为”张三”的同学参与了班长竞选投票,而投票时默认名字相同就是同一个人,如果现在基于”张三”字符串创建了多个String对象进行了投票,这些投票理应归票到同一个人身上,但是如果名字相同但hashcode不同就会出现最后取出的归票结果比实际上少的情况。

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private String name;
    private Integer age;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return Objects.equals(name, user.name) && Objects.equals(age, user.age);
    }

    @Override
    public int hashCode() {
        return Objects.hash(UUID.randomUUID());
    }
}
public class HashMapDemo {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        map.put(new String("张三"), 1);
        map.put(new String("张三"), map.get(new String("张三")) + 1);
        System.out.println(map.get(new String("张三"))); // 2

        Map<User, Integer> userMap = new HashMap<>();
        User zhangsan = new User("张三", 20);
        User zhangsan2 = new User("张三", 20);
        userMap.put(zhangsan, 1);
        userMap.put(zhangsan2, userMap.getOrDefault(zhangsan2, 0) + 1);
        System.out.println(userMap.getOrDefault(zhangsan, 0));
    }
}

hasCode取随机数,这时结果为0,与逻辑相悖。

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private String name;
    private Integer age;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return Objects.equals(name, user.name) && Objects.equals(age, user.age);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

修改hashCode逻辑,取name和age的hash值,这样上面代码运行结果就是2,符合逻辑。

总结:重写equals的同时重写hashCode方法就是为了确保hashMap等需要进行计算hash值的数据结构能够被正确使用。

受检异常和非受检异常

  • 受检异常。
    • 定义。受检异常是在编译前强制检查的异常。如果代码中存在可能抛出受检异常的代码,且没有被正确的处理(try-catch或在方法签名处抛出throws抛出),则会在编译阶段报错,导致程序无法通过编译。
    • 典型受检异常。IOException、InterruptedException
    • 处理方法。try-catch或在方法签名处抛出throws抛出。
  • 非受检时异常。包括RuntimeException和它的子类、Error和它的子类。
    • 定义。不需要强制处理,通常由编程错误引起。
    • 典型非受检异常。ArrayIndexOutOfBoundsException、NullPointerException等

JDK动态代理为什么只能代理有接口的类

这是由JDK动态代理的设计机制决定的。

java中动态代理是通过Proxy.newProxyInstance方法实现的,需要传入实现类的接口类,之所以传入接口不传入类是由JDK动态代理的底层实现决定的。

在程序运行期间JDK动态代理会动态生成一个$proxy的类,该类继承了Proxy父类,同时还会实现被代理类的接口,java中是不支持多继承的,而动态代理类必须继承Proxy类,所以导致JDK动态代理类只能代理接口而不能代理实现类。

动态代理实现需求就是对原始实现的拦截,然后在实现上做功能增强或扩展。实际开发中都是面向接口开发,所以JDK动态代理是与大部分实际开发场景吻合的。如果有实现类的动态代理场景我们也能用CGLIB组件实现,它会动态生成一个代理类的子类,子类重写父类中非final的方法,在子类中拦截父类所有方法的调用,从而实现动态代理。

HashMap是如何解决hash冲突的?

结论:HashMap采用了拉链法解决hash冲突问题。

HashMap底层是由数组实现的,初始容量为16,当添加元素时,会根据key值计算hash值,再取模得到索引值,并存储在数组对应索引位置,这时就可能会发生hash冲突,而hashMap的设计者采用的是拉链法进行解决的。

  • 链表。在hash冲突初期使用的是链表存储。
  • 红黑树。在链表过长时,会降低数据检索效率,为了时间复杂度,当链表长度大于8时采用红黑树结构存储。

零拷贝是什么?

零拷贝是一种计算机传在输文件时的I/O优化技术。

实际应用中,如果我们想把磁盘中的文件传输到远程服务器上,需要经过以下几个过程。

  1. 将磁盘中的文件拷贝到内核缓冲区中。
  2. 将内核缓冲区中的内容拷贝到用户缓冲区中。
  3. 将用户缓冲区中的内容拷贝到Socket buffer中。
  4. 将Socket buffer中的内容拷贝到网卡缓冲区。
  5. 网卡缓冲区再将数据传输到目标服务器上。

这几个流程存在4次拷贝操作,且涉及到用户态和内核态的切换。

使用零拷贝技术,可以不经历用户缓冲区的数据拷贝,直接从内核层面将数据拷贝到Socket缓冲区中,节省了两次数据拷贝的时间,且不用经过用户态到内核态的切换,节省了上下文切换的时间,从而提高了整体操作速度。

零拷贝实现:

linux中,零拷贝技术依赖于底层的sendfile()方法,而在java中FileChannel.transfer方法的底层实现逻辑就是sendFile()方法。

Integer使用不当导致的生产事故

场景:需要对常用范围(-127-128)Integer对象进行==比较时。

Integer类采用了享元模式的设计,当通过valueOf创建Integer对象,且当数值范围在-127-128之间相同值对象进行比较时,会返回true,但是如果值范围不在-127-128之间,则会返回false。

这是因为当首次使用Integer.valueOf静态方法时,静态内部类IntegerCache会初始化,它会创建-127-128之间的Integer对象作为缓存。

所以当使用Integer.valueOf获取-127-128之间的Integer对象时,会直接从缓存中取对应的Integer对象,而不会重新创建一个新的Integer对象。

这样在通过这种方式获取的Integer对象上进行==比较时:

  • 如果范围在-127-128之间,则返回true,说明是同一个对象。
  • 如果范围在-127-128之外,则返回false,说明不是同一个对象。

以下是代码实现:

public class IntegerDemo {
    public static void main(String[] args) {
        System.out.println(Integer.valueOf(1) == Integer.valueOf(1)); // true
        System.out.println(Integer.valueOf(129) == Integer.valueOf(129)); // false

        System.out.println(Long.valueOf(1) == Long.valueOf(1)); // true
        System.out.println(Long.valueOf(129) == Long.valueOf(129)); // false

        Integer i1 = new Integer(1);
        Integer i2 = new Integer(2);

        System.out.println(i1 == i2); // false

        // 做了装箱操作,相当于Integer.valueOf(100)
        Integer i3 = 100;
        Integer i4 = 100;
        System.out.println(i3 == i4); // true
    }
}

HashMap的hash方法为什么要右移16位

对原有哈希码做了一次再哈希,提高散列度,降低hash冲突,提升检索效率。

传统取模运算的情况下,计算出的hash值会与数组长度进行取模运算,得到在数组中的存储位置,这样哈希冲突的概率较高,元素会集中分布在一个小区间中。

而再哈希方法,会把hash值和hash值的高16位进行异或运算,得到再哈希值,最后再与数组长度进行取模运算,这样最终hash值也会收到高位信息的影响,从而提高散列度,降低hash冲突的概率。

java反射的优缺点

优点:

  • 增加程序灵活性,可以在运行过程中动态对类进行修改和操作。
  • 提高代码的复用性,比如动态代理,就是用到了反射来实现。
  • 可以在运行时轻松获取任意一个类的方法、属性,并且还能通过反射进行动态调用。

缺点:

  • 反射会涉及到动态类型的解析,所以JVM无法对这些代码进行优化,导致性能要比非反射调用更低。
  • 使用反射以后,代码的可读性会下降。
  • 反射会绕过访问限制,会破坏代码的封装性,比如用反射调用单例类的构造方法破坏单例特性。

设计模式有哪些?都有什么特点?

根据设计目标可以将设计模式分为三类:

创建型模式、结构型模式和行为型模式。

  • 创建型模式。是对象创建过程中出现的各种问题的解决方案的总结。
    • 工厂模式。
    • 单例模式。
    • 构建器模式。
    • 原型模式。
  • 结构型模式。通过继承和组合方式实现不同结构来解决各种场景问题的总结。
    • 桥接模式。
    • 适配器模式。
    • 装饰器模式。
    • 代理模式。
    • 组合模式。
    • 外观模式。
  • 行为模式。
    • 命令模式。
    • 解释器模式。
    • 策略模式。
    • 观察者模式。

HashMap什么时候扩容?怎么扩容?

扩容时机

一般来说所有集合容器(例如list、set等)都有相应的扩容机制,它们都会在存储数量达到临界值时进行扩容,在HashMap中这个临界值是基于负载因子和容量大小进行计算的,具体的threashold = loadFactor * capacity,其中负载因子默认为0.75而HashMap的初始容量为16,就是在元素个数达到12时就会发生扩容,扩容后的大小为原有容量的两倍。

如何扩容

扩容时会创建一个更长的数组,并将原有数组的元素拷贝到新数组中就能完成扩容了。

负载因子为什么默认是0.75

负载因子的大小有两个影响因素:

  1. hash冲突发生的概率。当负载因子过大时,hash冲突发生的概率就会越大,会降低元素的检索效率。
    • 当负载因子为0.75时,链表长度达到8的概率几乎为零。
  2. 空间使用率。当负载因子过小时,数组的空间使用率较低,会浪费大量内存空间。

强引用、弱引用、软引用和虚引用的区别

在java中,强引用、弱引用、软引用和虚引用是不同类型的引用,用于管理对象的生命周期。它们的区别主要在于引用对象被垃圾回收的条件和时机。

  • 强引用。最常见的引用类型,通常通过赋值操作创建。当一个对象具有强引用时,即使内存不足也不会回收。
  • 软引用通过SoftReference类来表示,用于描述那些内存不是必需但仍然有用的对象。当内存不足时才会回收。一般用于实现高速缓存等场景。
  • 弱引用通过WeakReference类来表示。软引用并不会阻止对象被垃圾回收。如果一个对象只有弱引用指向它,那么垃圾回收器会在下一次运行时回收该对象。弱引用通常用于构建可以在对象不再被强引用时自动释放的数据结构,如哈希表的键。
  • 虚引用通过PhantomReference类来表示。虚引用用于监控对象被垃圾回收的情况,但本身并不阻止对象被回收。虚引用通常与ReferenceQueue一起使用,当对象被回收时,虚引用会被放入引用队列中,以便应用程序可以了解到对象何时被回收。

java有哪几种文件拷贝方式?哪一种最高效?

  • java.io包下的库。使用FileInputStream来读取文件,并用FileOutputStream来写入文件。
  • java.nio包下的库。transferTo和transferFrom来实现。
  • java标准类库提供的Files.copy实现。

传统IO操作会调用操作系统内核提供的IO接口read()和write()方法,当用户线程调用read()时,会抛出一个异常,将文件操作委托给内核线程进行处理。在内核线程拿到CPU时间片后,会捕获这个异常,并且执行read()方法,将磁盘中的文件拷贝到内核的IO缓冲区中,然后将文件拷贝到jvm中的用户缓冲区中,这样的文件读取和写入方式效率较低。而transferTo()和transferFrom()方法是零拷贝的一种实现,它让文件拷贝直接在内核空间中进行操作,避免了不必要的用户内存和内核内存之间的拷贝和上下文切换,提高了拷贝效率,是三种中最高效的一种拷贝方式。

为什么阿里手册中规定要用包装类型来定义属性?

此开发规范为了提高程序后续的可维护性并避免一些低级错误而提出的,主要体现在以下几个方面:

  1. 默认值问题。基本数据类型有各自的默认值,比如int默认值为0,在某些场景下0值并不能明确的表明属性的状态。而包装类型初始值是null,表示该对象为空,并没有被赋过值,能更清晰的表达属性的状态。
  2. 特定场景下的装箱和拆箱操作带来的性能和可读性问题。使用包装类型能提高代码的可读性。
  3. java中的范型只能使用对象类型。如果要在编码中使用范型,就必须使用基本类型的包装类型。
  4. 包装类型提供了基本数据类型不具备的方法,例如equals(),hashCode(),toString()等,在特定场景下比较有用。

java有符号int最大值加一等于什么?

java有符号负数的计算方式是采用补码的形式表示的:

转换方式为:负数=正数的反码+1

例如:5 = 0000 0101,-5 = 1111 1011

-5 + 1得到-4,-4 = 1111 1100 取补码的确就是4的二进制表示(00000100)

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

Press ESC to close