Java 常见面试题

2022-05-09 15:41:38 浏览数 (1)

一、搜索

1、什么是Solr

Solr是一个Java开发的基于Lucene的 企业级 开源 全文搜索 平台。 它采用的是反向索引,即从关键字到文档的映射过程。 Solr的资源以Document为对象进行存储,每个文档由一系列的 Field 构成,每个Field 表示资源的一个属性。 文档的Field可以被索引, 以提工高性能的搜索效率。 一般情况下文档都包含一个能唯一表示该文档的id字段。

2、什么是Lucene

Lucene是apache软件基金会发布的一个开放源代码的全文检索引擎工具包,Lucene是根据关健字来搜索的文本搜索工具,只能在某个网站内部搜索文本内容,不能跨网站搜索

3、Solr的倒排索引

倒排索引就是从文档内容到文档序号的过程,将文档内容用solr自带分词器进行分词,然后作为索引,用二分法将关键字与排序号的索引进行匹配,进而查找到对应文档。倒排索引相对于正排而言,正排是从key查询value的一个过程,而倒排索引则是根据value查询key的一个过程,solr首先将数据进行分析,然后创建索引,将创建好的索引存储起来,查询时利用二分法去查询value,这个value对应一个Key,然后将这个Key返回。

4、 Solr和elasticsearch的区别?

https://www.cnblogs.com/jajian/p/9801154.html

共同点:solr和elasticsearch都是基于Lucene实现的!

不同点:

A. solr利用zookeeper进行分布式管理,而elasticsearch自身带有分布式协调管理功能;

B. solr比elasticsearch实现更加全面,solr官方提供的功能更多,而elasticsearch本身更注 重于核心功能,高级功能多由第三方插件提供;

C. solr在传统的搜索应用中表现好于elasticsearch,而elasticsearch在实时搜索应用方面比solr表现好!

二、Java基础知识

1、native关键字:

https://www.cnblogs.com/qian123/p/5702574.html

2、Java异常:

https://www.cnblogs.com/Qian123/p/5715402.html

3、static,final,this,super关键字总结:

https://gitee.com/SnailClimb/JavaGuide/blob/master/docs/java/basic/final,static,this,super.md

4、集合总结

https://www.cnblogs.com/skywang12345/p/3323085.html

1、fail-fast机制

ArrayList在用Iterator遍历数据时会产生fail-fast,导致异常,这是因为ArrayList遍历时会检查当前List的预期值和实际值是否相同,如果相同,则遍历,如果不同则产生异常

解决方案:

使用CopyOnWriteArrayList,他没有继承AbstractList,而是重写了iterator方法,遍历时新建一个数组,然后保存当前数据,由于是新数组,所以不会产生不一样的情况,自然不会有异常

2、HashMap详解:

https://zhuanlan.zhihu.com/p/21673805

3、我对于集合的理解

(1)、集合工具类Arrays

https://blog.csdn.net/samjustin1/article/details/52661862

代码语言:javascript复制
//asList方法可以将数据转化为List
        List<String> list = Arrays.asList("壮壮", "小马","小六");
        for (String s : list) {
            System.out.println(s);
        }

        //binarySearch可以在数组中查找是否存在某个元素,存在则返回该元素的下标
        int[] arr = new int[]{1, 3, 4,56,4,5,1, 56, 67};
        int index = Arrays.binarySearch(arr, 3,6,4);
        System.out.println(index);

        //sort可以将数组中元素排序
        //toString可以将数组元素打印输出 
        String[] names = {"Eric", "John", "Alan", "Liz"};
        Arrays.sort(names);
        System.out.println(Arrays.toString(names));

        int[][] stuGrades = { { 80, 81, 82 }, { 84, 85, 86 }, { 87, 88, 89 } };
        System.out.println(Arrays.deepToString(stuGrades));

        //fill可以将数组中的元素填充为指定元素
        int[] arr2 = new int[8];
        Arrays.fill(arr, 78);
        System.out.println(Arrays.toString(arr));

(2)、集合工具类Collections

https://www.cnblogs.com/nayitian/p/3269585.html

(3)、ArrayList

继承关系:

代码语言:javascript复制
java.lang.Object
   ↳     java.util.AbstractCollection<E>
         ↳     java.util.AbstractList<E>
               ↳     java.util.ArrayList<E>

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {}

特点:底层是数组实现,查找快,增删慢,它是线程不安全的,如果需要线程安全,则可以使用Vector

定义:它继承于AbstractList,实现了List, RandomAccess, Cloneable

ArrayList包含了两个重要的对象:elementData 和 size。

(01) elementData 是"Object[]类型的数组",它保存了添加到ArrayList中的元素。实际上,elementData是个动态数组,我们能通过构造函数 ArrayList(int initialCapacity)来执行它的初始容量为initialCapacity;如果通过不含参数的构造函数ArrayList()来创建ArrayList,则elementData的容量默认是10。elementData数组的大小会根据ArrayList容量的增长而动态的增长,具体的增长方式,请参考源码分析中的ensureCapacity()函数。

(02) size 则是动态数组的实际大小。

遍历:

遍历ArrayList时,使用随机访问(即,通过索引序号访问)效率最高,而使用迭代器的效率最低!

代码语言:javascript复制
//随机访问
Integer value = null;
int size = list.size();
for (int i=0; i<size; i  ) {
    value = (Integer)list.get(i);        
}

(4)、LinkedList

继承关系:

代码语言:javascript复制
java.lang.Object
   ↳     java.util.AbstractCollection<E>
         ↳     java.util.AbstractList<E>
               ↳     java.util.AbstractSequentialList<E>
                     ↳     java.util.LinkedList<E>

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable {}

特点:

底层是链表实现的,增删快,查找慢

简介:

LinkedList 是一个继承于AbstractSequentialList的双向链表。它也可以被当作堆栈、队列或双端队列进行操作。 LinkedList 实现 List 接口,能对它进行队列操作。 LinkedList 实现 Deque 接口,即能将LinkedList当作双端队列使用。 LinkedList 实现了Cloneable接口,即覆盖了函数clone(),能克隆。 LinkedList 实现java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输。 LinkedList 是非同步的。

定义:

LinkedList的本质是双向链表。 (01) LinkedList继承于AbstractSequentialList,并且实现了Dequeue接口。 (02) LinkedList包含两个重要的成员:header 和 size。   header是双向链表的表头,它是双向链表节点所对应的类Entry的实例。Entry中包含成员变量: previous, next, element。其中,previous是该节点的上一个节点,next是该节点的下一个节点,element是该节点所包含的值。   size是双向链表中节点的个数。

遍历:

遍历LinkedList时,使用removeFist()或removeLast()效率最高。但用它们遍历时,会删除原始数据;若单纯只读取,而不删除,应该使用第3种遍历方式。 无论如何,千万不要通过随机访问去遍历LinkedList!

代码语言:javascript复制
//随机访问效率最慢
int size = list.size();
for (int i=0; i<size; i  ) {
    list.get(i);        
}

(5)、vector

Vector简介

Vector 是矢量队列,它是JDK1.0版本添加的类。继承于AbstractList,实现了List, RandomAccess, Cloneable这些接口。 Vector 继承了AbstractList,实现了List;所以,它是一个队列,支持相关的添加、删除、修改、遍历等功能。 Vector 实现了RandmoAccess接口,即提供了随机访问功能。RandmoAccess是java中用来被List实现,为List提供快速访问功能的。在Vector中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访问。 Vector 实现了Cloneable接口,即实现clone()函数。它能被克隆。

和ArrayList不同,Vector中的操作是线程安全的

特点:底层是数组实现,是线程安全的,Vector里面的方法加上了synchronized关键字

继承关系:

代码语言:javascript复制
java.lang.Object
   ↳     java.util.AbstractCollection<E>
         ↳     java.util.AbstractList<E>
               ↳     java.util.Vector<E>

public class Vector<E>
    extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {}

Vector的数据结构和ArrayList差不多,它包含了3个成员变量:elementData , elementCount, capacityIncrement。

(01) elementData 是"Object[]类型的数组",它保存了添加到Vector中的元素。elementData是个动态数组,如果初始化Vector时,没指定动态数组的>大小,则使用默认大小10。随着Vector中元素的增加,Vector的容量也会动态增长,capacityIncrement是与容量增长相关的增长系数,具体的增长方式,请参考源码分析中的ensureCapacity()函数。

(02) elementCount 是动态数组的实际大小。

(03) capacityIncrement 是动态数组的增长系数。如果在创建Vector时,指定了capacityIncrement的大小;则,每次当Vector中动态数组容量增加时>,增加的大小都是capacityIncrement。

遍历:

遍历Vector使用索引的方式随机访问最快,使用迭代器最慢

(6)HashMap

HashMap简介

HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。 HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。 HashMap 的实现不是同步的,这意味着它不是线程安全的。它的key、value都可以为null。此外,HashMap中的映射不是有序的。

HashMap 的实例有两个参数影响其性能:“初始容量” 和 “加载因子”。容量 是哈希表中桶的数量,初始容量 只是哈希表在创建时的容量。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。 通常,默认加载因子是 0.75, 这是在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。

继承关系:

代码语言:javascript复制
java.lang.Object
   ↳     java.util.AbstractMap<K, V>
         ↳     java.util.HashMap<K, V>

public class HashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable { }

特点:

HashMap是数组加链表的方式实现的,首先根据hash值计算出元素存储的位置,如果该位置上已经有元素,则判断key是否相同,如果相同则覆盖,如果不同则在该节点创建链表,当链表长度超过8时,将链表改为红黑树

(7)、HashTable

特点:线程安全

(8)、TreeMap

特点:有序的key-value集合,它是通过红黑树实现的

(9)、set

set和list集合相同,但是set集合中不允许有重复的元素

HashSet:无序集合

TreeSet:有序集合

5、多线程

(1)、创建多线程
代码语言:javascript复制
//创建多线程有两种方法
//第一种 实现Runnable接口
class MyThread implements Runnable{
    @Override//重写run方法
    public void run() {
        for (int i = 0; i < 50; i  ) {
            System.out.println(Thread.currentThread().getName() i);
        }
    }
}

//第二种方法 继承Thread类
class MyThread extends Thread{
    //Thread的底层也是通过实现Runnable接口来实现多线程,并且Thread类的run方法调用的是接口的方法,因此继承Thread也需要实现run方法
    @Override//重写run方法
    public void run() {
        for (int i = 0; i < 50; i  ) {
            System.out.println(Thread.currentThread().getName() i);
        }
    }
}
(2)、Join方法

join()方法可以让一个线程强制运行

代码语言:javascript复制
MyThread thread = new MyThread();
        Thread t = new Thread(thread);
        t.start();
        for (int i = 0; i < 50; i  ) {
            if (i > 10) {
                try {
                    t.join();  //程序开始时,t线程和main线程交替运行,当i>10时,main停止运行,当t线程运行完成后,main线程才可以接着运行,Join()方法就是一个线程强制运行直至其死亡
                } catch (InterruptedException e) {
                }

            }
            System.out.println("Main线程开始运行" i);
        }
(3)、线程的状态

1、创建状态

Thread thread=new Thread(); 当程序new Thread(),说明该线程处于创建状态

2、就绪状态

t.start当线程调用start方法时,该线程则处于就绪状态,等待着CPU的调度

3、运行状态

线程获得了CPU和相应资源,开始运行run方法

4、阻塞状态

线程由于某种原因暂时停止运行,sleep(),suspend(),wait()等方法都可以让线程处于阻塞状态

5、死亡状态

线程运行完毕,或者线程调用stop方法,则该线程处于死亡状态

(4)、几个常用方法

1、join方法 强制线程运行

在线程操作中,可以使用 join() 方法让一个线程强制运行,线程强制运行期间,其他线程无法运行,必须等待此线程完成之后才可以继续执行。

2、sleep方法 休眠线程

在程序中允许一个线程进行暂时的休眠,直接使用 Thread.sleep() 即可实现休眠。

3、interrupt方法 中断线程

当一个线程运行时,另外一个线程可以直接通过interrupt()方法中断其运行状态。

4、setDaemon方法 后台线程

在 Java 程序中,只要前台有一个线程在运行,则整个 Java 进程都不会消失,所以此时可以设置一个后台线程,这样即使 Java 线程结束了,此后台线程依然会继续执行,要想实现这样的操作,直接使用 setDaemon() 方法即可。

5、setPriority方法

设置线程优先级 值分别为Thread.MIN_PRIORITY Thread.MAX_PRIORITY Thread.NORM_PRIORITY 范围1-10

6、yield方法 线程礼让

在线程操作中,也可以使用 yield() 方法将一个线程的操作暂时让给其他线程执行

(5)、start()和run()区别
代码语言:javascript复制
//这里调用run方法并没有新建线程,而是直接用当前线程
thread.run();
//start方法则新建了一个线程来调用run
thread.start();

// Demo.java 的源码
class MyThread extends Thread{  
    public MyThread(String name) {
        super(name);
    }

    public void run(){
        System.out.println(Thread.currentThread().getName() " is running");
    } 
}; 

public class Demo {  
    public static void main(String[] args) {  
        Thread mythread=new MyThread("mythread");

        System.out.println(Thread.currentThread().getName() " call mythread.run()");
        mythread.run();

        System.out.println(Thread.currentThread().getName() " call mythread.start()");
        mythread.start();
    }  
}

结果:
main call mythread.run()
main is running
main call mythread.start()
mythread is running
(6)、synchronized

一、原理:

在java中,每一个对象有且仅有一个同步锁。这也意味着,同步锁是依赖于对象而存在。 当我们调用某对象的synchronized方法时,就获取了该对象的同步锁。例如,synchronized(obj)就获取了“obj这个对象”的同步锁。 不同线程对同步锁的访问是互斥的。也就是说,某时间点,对象的同步锁只能被一个线程获取到!通过同步锁,我们就能在多线程中,实现对“对象/方法”的互斥访问。 例如,现在有两个线程A和线程B,它们都会访问“对象obj的同步锁”。假设,在某一时刻,线程A获取到“obj的同步锁”并在执行一些操作;而此时,线程B也企图获取“obj的同步锁” —— 线程B会获取失败,它必须等待,直到线程A释放了“该对象的同步锁”之后线程B才能获取到“obj的同步锁”从而才可以运行。

二、sychronized使用规则

我们将synchronized的基本规则总结为下面3条,并通过实例对它们进行说明。 第一条: 当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程对“该对象”的该“synchronized方法”或者“synchronized代码块”的访问将被阻塞。 第二条: 当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程仍然可以访问“该对象”的非同步代码块。 第三条: 当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程对“该对象”的其他的“synchronized方法”或者“synchronized代码块”的访问将被阻塞。

三、实例锁和全局锁(对象锁和类锁)

实例锁:当时使用synchronized方法或者sychronized代码块时,如果不加上static的话,那么这个锁就是实例锁(对象锁),也就是说同一个对象共享同一个实例锁,不同的对象享受不同的实例锁,互不干涉。 全局锁:当使用static synchronized实现同步时,那么就是全局锁,即同一个类共享一个锁,不管有多少对象,只要他们属于同一个类,那么这些对象都共享一个锁

(7)、wait和notify

一、介绍

在Object.java中,定义了wait(), notify()和notifyAll()等接口。wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。而notify()和notifyAll()的作用,则是唤醒当前对象上的等待线程;notify()是唤醒单个线程,而notifyAll()是唤醒所有的线程。 Object类中关于等待/唤醒的API详细信息如下: notify() -- 唤醒在此对象监视器上等待的单个线程。 notifyAll() -- 唤醒在此对象监视器上等待的所有线程。 wait() -- 让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法”,当前线程被唤醒(进入“就绪状态”)。 wait(long timeout) -- 让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量”,当前线程被唤醒(进入“就绪状态”)。 wait(long timeout, int nanos) -- 让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量”,当前线程被唤醒(进入“就绪状态”)。

二、注意点:

  1. wait()方法的作用是让当前线程等待,当调用wait方法后,当前线程释放同步锁,其他线程可以获取该同步锁执行
  2. 只有和持有该对象同步锁的线程执行了notify()方法并且释放了同步锁以后,wait线程才可以获取锁继续执行

三、示例:

代码语言:javascript复制
// WaitTest.java的源码
class ThreadA extends Thread{

    public ThreadA(String name) {
        super(name);
    }

    public void run() {
        synchronized (this) {
            System.out.println(Thread.currentThread().getName() " call notify()");
            // 唤醒当前的wait线程
            notify();
        }
    }
}

public class WaitTest {

    public static void main(String[] args) {

        ThreadA t1 = new ThreadA("t1");

        synchronized(t1) {
            try {
                // 启动“线程t1”
                System.out.println(Thread.currentThread().getName() " start t1");
                t1.start();

                // 主线程等待t1通过notify()唤醒。
                System.out.println(Thread.currentThread().getName() " wait()");
                t1.wait();

                System.out.println(Thread.currentThread().getName() " continue");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

/**
结果:
main start t1
main wait()
t1 call notify()
main continue

结果说明:
如下图,说明了“主线程”和“线程t1”的流程。

(01) 注意,图中"主线程" 代表“主线程main”。"线程t1" 代表WaitTest中启动的“线程t1”。 而“锁” 代表“t1这个对象的同步锁”。
(02) “主线程”通过 new ThreadA("t1") 新建“线程t1”。随后通过synchronized(t1)获取“t1对象的同步锁”。然后调用t1.start()启动“线程t1”。
(03) “主线程”执行t1.wait() 释放“t1对象的锁”并且进入“等待(阻塞)状态”。等待t1对象上的线程通过notify() 或 notifyAll()将其唤醒。
(04) “线程t1”运行之后,通过synchronized(this)获取“当前对象的锁”;接着调用notify()唤醒“当前对象上的等待线程”,也就是唤醒“主线程”。
(05) “线程t1”运行完毕之后,释放“当前对象的锁”。紧接着,“主线程”获取“t1对象的锁”,然后接着运行。
*/
(8)、线程优先级

一、定义

用户线程:用户线程一般用于执行用户任务 守护线程:守护线程一般用于执行后台任务 当用户线程全部执行完毕后,守护线程也会自动结束。 我们可以用isDaemon()方法判断线程是否为守护线程,当isDaemon()返回false时,表明该线程为用户线程,可以用setDaemon()方法将一个线程设为守护线程

二、线程优先级

线程优先级范围1-10,数字越大,说明线程优先级越高,默认这个数字为5。优先级高的优先于优先级低执行

三、算法

1、链表:

Java链表的实现:https://blog.csdn.net/jianyuerensheng/article/details/51200274

tips:双指针法,快慢指针(一个指针速度慢,一个指针速度快)

eg1:查找链表中间元素

fast指针每次往后走两步,slow每次向后走一步,这样当fast走到链表结尾时,slow刚好走到链表的中间位置

代码语言:javascript复制
class Solution {
public:
    ListNode* getKthFromEnd(ListNode* head, int k) {
        ListNode *p = head, *q = head; //初始化
        while(k--) {   //将 p指针移动 k 次
            p = p->next;
        }
        while(p != nullptr) {//同时移动,直到 p == nullptr
            p = p->next;
            q = q->next;
        }
        return q;
    }
};

eg2:倒数第k个元素问题

先来看"倒数第k个元素的问题"。设有两个指针 p 和 q,初始时均指向头结点。首先,先让 p 沿着 next 移动 k 次。此时,p 指向第 k 1个结点,q 指向头节点,两个指针的距离为 k 。然后,同时移动 p 和 q,直到 p 指向空,此时 p 即指向倒数第 k 个结点

代码语言:javascript复制
public:
    ListNode* middleNode(ListNode* head) {
        ListNode *p = head, *q = head;
        while(q != nullptr && q->next != nullptr) {
            p = p->next;
            q = q->next->next;
        }
        return p;
    } 
};

eg3:链表是否存在环

类似于两个人在操场上跑步,其中一个快,一个慢,那么最终快和慢的一定会相遇

代码语言:javascript复制
public:
    bool hasCycle(ListNode *head) {
        ListNode *slow = head;
        ListNode *fast = head;
        while(fast != nullptr) {
            fast = fast->next;
            if(fast != nullptr) {
                fast = fast->next;
            }
            if(fast == slow) {
                return true;
            }
            slow = slow->next;
        }
        return nullptr;
    }
};

四:Redis

1、Redis持久化:

RDB和AOF

2、Redis事务

MULTI:开启事务 EXEC:提交事务 DISCARD:放弃事务

开启事务后,所有操作命令都会加入到队列中,并不会立即执行,等到执行EXEC时再统一执行命令

1、正常执行:当所有命令正常执行时,提交事务时会将所有在队列中的命令正常提交,然后执行 2、放弃事务:执行DISCARD命令则放弃本次事务 3、全体连坐:有一个出错,全部都不提交 4、Watch监控:Watch监控时如果有其他人修改了数据,那么此次事务提交失败,需要获取到最新的数据才能继续

五、网络

TCP三次握手

1、首先客户端发送一个SYN=1和seq=x包给服务器

2、服务器确认收到后,发送给客户端SYN=1,ack=x 1,seq=y(这个是服务器端的序列号)

3、客户端收到服务器端的通知后发送ACK=1,seq=x 1,ack=y 1

TCP四次挥手

0 人点赞