面试准备:Java常见面试题汇总(二)

文章目录

43.java 中的 Math.round(-1.5) 等于多少?

Math.round采用了四舍五入,四舍五入的原理是在参数上加0.5然后做向下取整。
Math.round(1.5)的返回值是2,Math.round(-1.5)的返回值是-1。

44.String str="i"与 String str=new String(“i”)一样吗?

不一样,因为内存的分配方式不一样。String str="i"的方式,java虚拟机会将其分配到常量池中;而String str=new String(“i”)则会被分到堆内存中。

45.如何将字符串反转?

1.利用 StringBuffer 或 StringBuilder 的 reverse 成员方法:
2.利用 String 的 toCharArray 方法先将字符串转化为 char 类型数组,然后将各个字符进行重新拼接:

46.String 类的常用方法都有那些?

toCharArray()
substring()
charAt()
length()
split()
trim()
toUpperCase()

47.抽象类必须要有抽象方法吗?

抽象类可以没有抽象方法,但是如果你的一个类已经声明成了抽象类,即使这个类中没有抽象方法,它也不能再实例化,即不能直接构造一个该类的对象。如果一个类中有了一个抽象方法,那么这个类必须声明为抽象类,否则编译通不过。

48.普通类和抽象类有哪些区别?

抽象类用abstract修饰,且不能被实例化。
抽象类不能是final的。
抽象方法不可以使用private、final或者static。
抽象类的子类必须实现抽象类中所有抽象方法,否则这个子类也是抽象类。

49.java 中 IO 流分为几种?

Java中的流分为两种,一种是字节流,另一种是字符流。
字符流和字节流是根据处理数据的不同来区分的。

  • 字节流:
    操作的时候不会用到缓存区(内存)
    1个字节-8位。
    字节流可用于任何类型的对象,包括二进制对象。
  • 字符流:
    操作的时候会用到缓存区。
    一个字符char-16位。
    而字符流只能处理字符或者字符串。

字节输入流和输出流:InputStream和OutputStream
字符输入流和输出流:Reader和Writer
在这里插入图片描述

50.BIO、NIO、AIO 有什么区别?

51.Files的常用方法都有哪些?

Files.exists() 检测文件路径是否存在
Files.createFile()创建文件
Files.createDirectory()创建文件夹
Files.delete() 删除文件或者目录
Files.copy() 复制文件
Files.move() 移动文件
Files.size()查看文件个数
Files.read() 读取文件
Files.write()写入文件

52.Collection 和 Collections 有什么区别?

java.util.Collection 是一个集合接口。它提供了对集合对象进行基本操作的通用接口方法,比如add()、remove()、size()等等。

java.util.Collections 是一个包装类。它包含有各种有关集合操作的静态多态方法,比如sort()、reverse()、copy()、fill()。

53.说一下HashMap和HashSet的实现原理?

HashMap 的实现原理:
HashMap是基于Hash算法实现的,
我们通过put(key,value)存储数据,通过get(key)来获取数据

当传入key时,HashMap会根据Key.hashCode()计算出Hash值,根据Hash值将value保存在bucket里 ,。

当计算出相同的Hash值时,我们称之为Hash冲突,HashMap 的做法是用链表和红黑树存储相同Hash值的value,
当hash冲突的个数比较少时,使用链表存储,
否则使用红黑树。

HashSet 的实现原理:
HashSet是基于HashMap实现的,HashSet 底层使用HashMap来保存所有元素,
因此HashSet 的实现比较简单,相关HashSet 的操作,基本上都是直接调用底层HashMap的相关方法来完成,HashSet不允许有重复的值,并且元素是无序的。

54.如何实现数组和 List 之间的转换?

		String[] arr = new String[5];
        arr[0]="a";
        arr[1]="b";
        arr[2]="c";
        arr[3]="d";
        arr[4]="e";
        List<String> list = new ArrayList<>();
        List<String> list1 = Arrays.asList(arr);


        //将list集合转换为数组Array
        List<Integer> ls = new ArrayList<>();
        ls.add(1);
        ls.add(2);
        ls.add(3);
        ls.add(4);
        ls.add(5);
        Object[] objects = ls.toArray();

55.在 Queue 中 poll()和 remove()有什么区别?

poll()和remove()都将移除并且返回对头,但是在poll()在队列为空时返回null,而remove()会抛出NoSuchElementException异常。

56.ArrayList 和 Vector 的区别是什么?

ArrayList 和Vector底层是采用数组方式存储数据

Vector:
线程同步(因为效率较低,现在已经不太建议使用)
当Vector中的元素超过它的初始大小时,Vector会将它的容量翻倍,
ArrayList:
线程不同步,但性能很好
当ArrayList中的元素超过它的初始大小时,ArrayList只增加50%的大小

57.哪些集合类是线程安全的?

常见的并发集合:
ConcurrentHashMap:线程安全的HashMap的实现
CopyOnWriteArrayList:线程安全且在读操作时无锁的ArrayList
CopyOnWriteArraySet:基于CopyOnWriteArrayList,不添加重复元素
ArrayBlockingQueue:基于数组、先进先出、线程安全,可实现指定时间的阻塞读写,并且容量可以限制
LinkedBlockingQueue:基于链表实现,读写各用一把锁,在高并发读写操作都多的情况下,性能优于ArrayBlockingQueue

另外还有
Vector:就比Arraylist多了个同步化机制(线程安全)。
Hashtable:就比Hashmap多了个线程安全。
Stack:继承自Vector

58.迭代器 Iterator 是什么?

迭代器(Iterator) 迭代器是一种设计模式,它是一个对象,它可以遍历并选择序列中的对象。
迭代器可以使用remove()删除对象,而直接使用foreach是不行的。
另外如果使用for循环for(int i=0;i<list.size();i++),如果在中途删除了这个对象,会导致list.size()改变,使用也不是很方便。

	//使用 hasNext 和 next遍历 
	public static void testIteratorNext() {
		Iterator<String> iterator = list.iterator();
		while (iterator.hasNext()) {
			String str = iterator.next();
			System.out.println(str);
		}
	}
	
	//使用 Iterator 删除元素 
	public static void testIteratorRemove() {
		Iterator<String> iterator = list.iterator();
		while (iterator.hasNext()) {
			String str = iterator.next();
			if ("222".equals(str)) {
				iterator.remove();
			}
		}
		System.out.println(list);
	}

59.怎么确保一个集合不能被修改?

采用Collections包下的unmodifiableMap方法,通过这个方法返回的map,是不可以修改的。他会报 java.lang.UnsupportedOperationException错。

同理:Collections包也提供了对list和set集合的方法。
Collections.unmodifiableList(List)
Collections.unmodifiableSet(Set)

60.并发和并行有什么区别?

并发是指一个处理器同时处理多个任务。并行是指多个处理器或者是多核的处理器同时处理多个不同的任务。并发是逻辑上的同时发生(simultaneous),而并行是物理上的同时发生。

61.线程和进程之间的区别?

1、进程是一段正在执行的程序,是资源分配的基本单元,而线程是CPU调度的基本单元。
2、进程间相互独立,进程之间不能共享资源。一个进程至少有一个线程,同一进程的各线程共享整个进程的资源(寄存器、堆栈、上下文)。
3、线程的创建和切换开销比进程小,换句话说线程是轻量级的进程。

62.Java线程之间怎么通信?

两种通信机制:共享内存机制和消息通信机制。

  1. 共享内存机制

使用同步方法:
syncrhoized加锁的线程的Object类的wait()/notify()/notifyAll()
ReentrantLock类加锁的线程的Condition类的await()/signal()/signalAll()
CountDownLatch
CyclicBarrier

或者使用:轮询+volatile关键字

public class TestSync {
    // 定义一个共享变量来实现通信,它需要是volatile修饰,否则线程不能及时感知
    static volatile boolean notice = false;

    public static void main(String[] args) {
        List<String>  list = new ArrayList<>();
        // 实现线程A
        Thread threadA = new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                list.add("abc");
                System.out.println("线程A向list中添加一个元素,此时list中的元素个数为:" + list.size());
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (list.size() == 5)
                    notice = true;
            }
        });
        // 实现线程B
        Thread threadB = new Thread(() -> {
            while (true) {
                if (notice) {
                    System.out.println("线程B收到通知,开始执行自己的业务...");
                    break;
                }
            }
        });
        // 需要先启动线程B
        threadB.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 再启动线程A
        threadA.start();
    }
}
  1. 消息通信机制

使用java.io.PipedInputStream 和 java.io.PipedOutputStream进行通信。
一个线程发送数据到输出管道,另一个线程从输入管道读数据。

package pipeInputOutput;

import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;

public class Run {

    public static void main(String[] args) {

        try {
            WriteData writeData = new WriteData();
            ReadData readData = new ReadData();

            PipedInputStream inputStream = new PipedInputStream();
            PipedOutputStream outputStream = new PipedOutputStream();

            //将两个Stream之间产生通信链接,这样才能将数据进行输入输出,下面两种方式都可以,其一即可
            //inputStream.connect(outputStream);
            outputStream.connect(inputStream);

            //开启读线程
            ThreadRead threadRead = new ThreadRead(readData, inputStream);
            threadRead.start();

            Thread.sleep(2000);

            //开启写线程
            ThreadWrite threadWrite = new ThreadWrite(writeData, outputStream);
            threadWrite.start();

        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

}

63.守护线程是什么?

JAVA分为守护线程daemon thread和用户线程user thread。
当 JVM 中不存在任何一个正在运行的非守护线程(用户线程)时,则 JVM 进程即会退出。
换句话说,守护线程拥有自动结束自己生命周期的特性,而非守护线程不具备这个特点。

通常来说,守护线程经常被用来执行一些后台任务,但是呢,你又希望在程序退出时,或者说 JVM 退出时,线程能够自动关闭,此时,守护线程是你的首选。

JVM 中的垃圾回收线程就是典型的守护线程,如果说不具备该特性,会发生什么呢?
当 JVM 要退出时,由于垃圾回收线程还在运行着,导致程序无法退出。

64.创建线程有哪几种方式?

  1. 继承Thread类,并重写run()方法。
class Runner extends Thread{
    @Override
    public void run() {
        super.run();
        System.out.println("ok");
    }

    public static void main(String[] args) {
        new Runner().start();
    }
}
  1. 继承Runnable接口,并重写run()方法
class Runner implements Runnable{
    @Override
    public void run() {
        System.out.println("ok");
    }
    public static void main(String[] args) {
        new Thread(new Runner()).start();
    }
}

实际上Thread也是继承了run方法的。

  1. 通过Callable和Future创建线程。
class Runner implements Callable<String> {
    @Override
    public String call() throws Exception {
        System.out.println("运行ok");
        return "success";
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Runner runner=new Runner();
        FutureTask<String> futureTask=new FutureTask<>(runner);
        new Thread(futureTask).start();
        System.out.println("执行后返回值:"+futureTask.get());
    }
}
  1. 通过线程池
class Runner{
    public static void main(String[] args) {
        ExecutorService pool=Executors.newFixedThreadPool(2);
        for (int i=0;i<5;i++){
            pool.submit(()->{//可以使用Callable或者Runnable
                System.out.println("ok");
            });
        }
    }
}

65.说一下 runnable 和 callable 有什么区别?

1、两者最大的不同点是:实现Callable接口的任务线程能返回执行结果;而实现Runnable接口的任务线程不能返回结果;
2、Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛;

Callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取‘将来’结果;当不调用此方法时,主线程不会阻塞!

66.线程有哪些状态?

Java的源代码中6个状态分别是:New, Runnable, Blocked, Waiting, Timed_Waiting, Terminated.
线程状态分别是:新建、就绪、运行、阻塞、死亡。
在这里插入图片描述

  • NEW:线程对象创建之后、启动之前的状态。
  • RUNNABLE:调用start()方法后的状态。
  • TERMINATED:run()方法或者call()方法执行完毕后的状态。也有可能是发生Error/Exception或者调用了stop()之后的状态。
  • BLOCKED/WAITING/TIMED_WAITING:使用sleep()、IO阻塞、等待通知、同步锁之后的状态。(暂时不做区分)
class Runner extends Thread{
    private static volatile boolean go=true;
    @Override
    public void run() {
        super.run();
        while (go){

        }
        System.out.println("ok");
    }

    public static void main(String[] args) throws InterruptedException {
        Runner runner=new Runner();
        System.out.println(runner.getState());//NEW
        runner.start();
        System.out.println(runner.getState());//RUNNABLE
        go=false;
        Thread.sleep(100);
        System.out.println(runner.getState());//TERMINATED
    }
}

67.sleep() 和 wait() 有什么区别?

  1. 同步锁的对待不同:
    sleep()后,程序并不会不释放同步锁。
    wait()后,程序会释放同步锁。

  2. 用法的不同:
    sleep()可以用时间指定来使他自动醒过来。如果时间不到你只能调用interreput()来强行打断。
    wait()可以用notify()直接唤起。

  3. 属于不同的类:
    sleep()的类是Thread。
    wait()的类是Object。

68.notify()和 notifyAll()有什么区别?

notifyAll调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而notify只会随机唤醒一个线程。

锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。
等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池中

69.线程的 run()和 start()有什么区别?

调用start方法方可启动线程,而run方法只是thread的一个普通方法调用,还是在主线程里执行。

70.创建线程池有哪几种方式?

Executors目前提供了5种不同的线程池创建配置:

1、newCachedThreadPool(),它是用来处理大量短时间工作任务的线程池,具有几个鲜明特点:它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲置时间超过60秒,则被终止并移除缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用SynchronousQueue作为工作队列。

2、newFixedThreadPool(int nThreads),重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有nThreads个工作线程是活动的。这意味着,如果任务数量超过了活动线程数目,将在工作队列中等待空闲线程出现;如果工作线程退出,将会有新的工作线程被创建,以补足指定数目nThreads

3、newSingleThreadExecutor(),它的特点在于工作线程数目限制为1,操作一个无界的工作队列,所以它保证了所有的任务都是被顺序执行,最多会有一个任务处于活动状态,并且不予许使用者改动线程池实例,因此可以避免改变线程数目

4、newSingleThreadScheduledExecutor()和newScheduledThreadPool(int corePoolSize),创建的是个ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程。

5、newWorkStealingPool(int parallelism),这是一个经常被人忽略的线程池,Java 8 才加入这个创建方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序。

6、使用new ThreadPoolExecutor()自定义。

71.线程池都有哪些状态?

  1. Running: 接受新task, 处理等待的task;
  2. ShutDown: 不接受新task,但处理等待的task;
  3. Stop: 不接受新task, 不处理等待的task, 尝试打断正在执行的task;
  4. Tidying:但所有task都被终止, worCount == 0的时候(workCount是指有效的线程数);
  5. Terminated: 执行完terminated()方法;

  • Running -> ShutDown:
    执行shutdown();

  • Running or ShutDown --> Stop:
    执行shutdownNow();

  • Stop --> Tidying
    当pool为空时

  • ShutDown --> Tidying
    当queue 和 pool都为空时

  • Tidying --> Terminated
    当terminated()方法结束时;

72.线程池中 submit()和 execute()方法有什么区别?

public interface ExecutorService extends Executor {...}

可以看出ExecutorService接口继承自Executor,而这个Executor接口需要实现execute()方法。

public interface Executor {
    void execute(Runnable command);
}

execute()方法的入参为一个Runnable,返回值为void。

而我们再来看submmit()方法是怎么来的:

public interface ExecutorService extends Executor {
  ...
  <T> Future<T> submit(Callable<T> task);

  <T> Future<T> submit(Runnable task, T result);

  Future<?> submit(Runnable task);
  ...
}

它是ExecutorService的实现类实现的,入参可以为Callable<T>,也可以为Runnable,而且方法有返回值Future<T>

总结,从上面的源码以及讲解可以总结execute()和submit()方法的区别:

  1. 接收的参数不一样;
  2. submit()有返回值Future,而execute()没有;
  3. submit()可以进行Exception处理
    可以通过对Future.get()进行抛出异常的捕获,然后对其进行处理。

73.在 java 程序中怎么保证多线程的运行安全?

1.原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);

2.可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile);

3.有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before原则)。

JDK里面提供了很多atomic类,AtomicInteger,AtomicLong,AtomicBoolean等等。

74.多线程锁的升级原理是什么?

锁的级别从低到高:
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁

锁分级别原因:
没有优化以前,sychronized是重量级锁(悲观锁),使用 wait 和 notify、notifyAll 来切换线程状态非常消耗系统资源;线程的挂起和唤醒间隔很短暂,这样很浪费资源,影响性能。所以 JVM 对 sychronized 关键字进行了优化,把锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。

  • 无锁:
    没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其他修改失败的线程会不断重试直到修改成功。
    无锁主要采用CAS(还用到了内存屏障(有序性)、可见性和原子性)。

  • 偏向锁:
    对象的代码一直被同一线程执行,不存在多个线程竞争,该线程在后续的执行中自动获取锁,降低获取锁带来的性能开销。偏向锁,指的就是偏向第一个加锁线程,该线程是不会主动释放偏向锁的只有当其他线程尝试竞争偏向锁才会被释放
    偏向锁的撤销,需要在某个时间点上没有字节码正在执行时,先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁;如果线程处于活动状态,升级为轻量级锁的状态。

  • 轻量级锁:
    轻量级锁是指当锁是偏向锁的时候,被第二个线程 B 所访问,此时偏向锁就会升级为轻量级锁,线程 B 会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。
    当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定的次数时,轻量级锁便会升级为重量级锁;当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁。

  • 重量级锁:
    指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态
    重量级锁通过对象内部的监视器(monitor)实现,而其中 monitor 的本质是依赖于底层操作系统的 Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。

75.什么是死锁?怎么防止死锁?

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象。

死锁发生的原因:
1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
4、循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。

防止死锁:

  1. 有序资源分配法
    对它所必须使用的而且属于同一类的所有资源,必须一次申请完;在申请不同类资源时,必须按各类设备的编号依次申请。
  2. 银行家算法
    在避免死锁方法中允许进程动态地申请资源,但系统在进行资源分配之前,应先计算此次分配资源的安全性,若分配不会导致系统进入不安全状态,则分配,否则等待。

76.什么是 java 序列化?什么情况下需要序列化?

序列化:将 Java 对象转换成字节流的过程。
反序列化:将字节流转换成 Java 对象的过程。

当 Java 对象需要在网络上传输 或者 持久化存储到文件中时,就需要对 Java 对象进行序列化处理。

  • 网络传输:我们将系统拆分成多个服务之后,服务之间传输对象,不管是何种类型的数据,都必须要转成二进制流来传输,接受方收到后再转为数据对象。
  • 数据持久化:比如一个电商平台,有数万个用户并发访问的时候会产生数万个session 对象,这个时候内存的压力是很大的。我们可以把session对象序列化到硬盘中,需要时在反序列化,减少内存压力。

序列化的实现:类实现 Serializable 接口,这个接口没有需要实现的方法。实现 Serializable 接口是为了告诉 jvm 这个类的对象可以被序列化。

注意事项:
声明为 static 和 transient 的成员变量,不能被序列化。static 成员变量是描述类级别的属性,transient 表示临时数据

77.为什么要使用克隆?

开发过程中,有时会遇到把现有的一个对象的所有成员属性拷贝给另一个对象的需求。

浅拷贝介绍:
它会创建一个新对象,其成员变量是基本类型,拷贝的就是基本类型的值;如果属性是引用类型,拷贝的就是内存地址 。

深拷贝介绍:
(1) 对于基本数据类型的成员对象,因为基础数据类型是值传递的,所以是直接将属性值赋值给新的对象。基础类型的拷贝,其中一个对象修改该值,不会影响另外一个(和浅拷贝一样)。
(2) 对于引用类型,比如数组或者类对象,深拷贝会新建一个对象空间,然后拷贝里面的内容,所以它们指向了不同的内存空间。改变其中一个,不会对另外一个也产生影响。
(3) 对于有多层对象的,每个对象都需要实现 Cloneable 并重写 clone() 方法,进而实现了对象的串行层层拷贝。
(4) 深拷贝相比于浅拷贝速度较慢并且花销较大。

78.Java实现深拷贝的两种方式?

通过序列化和反序列化深拷贝参考:
原型模式

通过clone接口:

public class Demo implements Cloneable {
 
    private String name;
 
    private String value;
 
    private DemoInternal demoInternal;
 
    /*省略getter和setter方法*/
 
    @Override
    public Demo clone() {
        Demo demo = null;
        try {
            demo = (Demo) super.clone(); //浅复制
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        demo.demoInternal = demoInternal.clone(); //深度复制
        return demo;
    }
}
public class DemoInternal implements Cloneable {
 
    private String internalName;
 
    private String internalValue;
 
    /*省略getter和setter方法*/
 
    @Override
    public DemoInternal clone() {
        DemoInternal demoInternal = null;
        try {
            demoInternal = (DemoInternal) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return demoInternal;
    }

如果包含了数组,可以new一个新的数组来重新开辟空间。

发布了431 篇原创文章 · 获赞 329 · 访问量 17万+

猜你喜欢

转载自blog.csdn.net/No_Game_No_Life_/article/details/105141583