Java基础面试

115 阅读21分钟

1. == 和equals的区别是什么

1. == 解读

对于基本类型和引用类型 == 的作用效果是不同的,如下图所示:

  • 基本类型:比较的是值是否相同
  • 引用类型:比较的是引用是否相同

代码实例:

String x = "string";

String y = "string";

String z = new String("string");

System.out.println(x==y); // true

System.out.println(x==z); // false

System.out.println(x.equals(y)); // true

System.out.println(x.equals(z)); // true

代码解读:

因为 x 和 y指向的是同一个引用,所以==也是true,而new String()方法则重写开辟了内存空间,所以==结果为false,而equals比较的一直是值,所以结果都为true。

2. equals解读

equals本质上就是==,只不过String和Integer等重写了equals方法,把它变成了值比较。看下面的代码明白了。

首先看默认情况下equals比较一个有相同值的对象,代码如下:

class Cat {

public Cat(String name) {

this.name = name;
}
  
private String name;
public String getName() {

return name;

}
public void setName(String name) {

this.name = name;

}
}
Cat c1 = new Cat("王磊");

Cat c2 = new Cat("王磊");

System.out.println(c1.equals(c2)); // false

输出结果出乎我们的意料,竟然是false?这是怎么回事,看了equals源码知道,源码如下:

public boolean equals(Object obj) {

return (this == obj);

}

那么问题来了,两个相同值的String对象,为什么返回的true?

String s1 = new String("老王");

String s2 = new String("老王");

System.out.println(s1.equals(s2)); // true

同样的,当我们进入String的equals方法,找到了答案,代码如下:

public boolean equals(Object anObject) {

if (this == anObject) {

return true;

}

if (anObject instanceof String) {

String anotherString = (String)anObject;

int n = value.length;

if (n == anotherString.value.length) {

char v1[] = value;

char v2[] = anotherString.value;

int i = 0;

while (n-- != 0) {

if (v1[i] != v2[i])
return false;
i++;

}
return true;

}

}
return false;

}

原来是String重写了我们的Object的equals方法,把引用比较改成了值的比较。

总结:

==对于基本类型来说是值的比较,对于引用类型来说是比较的引用;而equals默认情况下是引用比较,只是很多类重新equals方法,比如String、Integer等把它变成了值比较,所以一般情况下equals比较的是值是否相等

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

不对,两个对象的hashCode()相同,equals()不一定true。

代码实例:

String str1 = "通话";

String str2 = "重地";

System. out. println(String. format("str1:%d | str2:%d",  str1. hashCode(),str2. hashCode()));

System. out. println(str1. equals(str2));

执行结果:

str1:1179395 | str2:1179395
false

代码解读:

很明显“通话”和“重地”的hashCode()相同,然而equals()则为false,因为在散列表中,hashCode()相等的两个键值对相等,然而哈希值相等,并不一定的出键值对相等

3. Java中操作字符串的都有哪些类?它们之间有什么区别?

操作字符串的类有:String、StringBuffer、StringBuilder

  • String和StringBuffer、StringBuilder的区别在于String 声明的是不可变的对象,每次操作都会生成的新的String对象,然后将指针指向新的String对象,而StringBuffer、StringBuilder可以在原有的对象基础上进行操作,所以在经常改变字符串内容的情况下不要使用String
  • StringBuilder和StringBuffer最大的区别在于,StringBuffer是线程安全的,而StringBuiler是非线程安全的,但StringBuilder的性能却高于StringBuffer,所以在单线程环境下推荐使用StringBuilder,多线程环境下推荐使用StringBuffer。

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

  • 普通类不能包含抽象方法,抽象类可以包含抽象方法
  • 抽象类不能被实例化,普通类可以直接实例化

5. 接口和抽象类有什么区别?

  • 实现:抽象类的子类使用extends来继承;接口:接口必须使用implements来实现接口。
  • 构造函数:抽象类可以有构造函数;接口不能有
  • 实现数量:类可以实现多个接口;但是只能继承一个抽象类
  • 访问修饰符:接口中的方法默认使用public修饰;抽象类中的方法可以任意访问修饰符。

6. Java中IO流分为几种?

  • 按功能分:输入流(input)、输出流(ouput)
  • 按类型分:字节流和字符流

字节流和字符流的区别是:字节流按8位传输以字节为单位输入输出数据,字符流按16位传输字符流位单位输入输出数据

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

  • BIO:Block IO 同步阻塞式IO,就是我们平常使用的传统IO,他的特点是模式简单使用方便,并处理能力低

  • NIO:New IO同步非阻塞式IO,是传统IO的升级,客户端和服务端通过Channel(通道)通讯,实现了多路复用

  • AIO:Asynchronous IO 是NIO的升级,也叫NIO2,实现异步非阻塞IO,异步IO的操作基于事件和回调机制

8. Java容器都有哪些?

Java容器分位Collction和Map两大类,其下又有很多字类,如下所示:

Collection:

List
  • ArrayList
  • LinkedList
  • Vector
  • Stack
Set
  • HashSet
  • LinkedHashSet
  • TreeSet

Map:

HashMap
  • LinkedHashMap
TreeMap
ConcurrentHashMap
Hashtable

9. Collection和Collections有什么区别?

Collection是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法,所有集合都是它的子类,比如:List、Set等

Collections是一个包装类,包含了很多静态方法,不能被实例化,就像一个工具类,比如提供的排序方法:Collections.sort(list)

10. List、Set、Map之间的区别是什么?

List、Set、Map之间的区别主要体现在两个方面:元素是否有序、是否允许元素重复。

三者之间的区别,如下表:

11. HashMap和Hashtable有什么区别?

存储:HashMap允许Key和Value为空,而Hashtable不允许。
线程安全:Hashtable是线程安全的,HashMap是非线程安全的
推荐使用:在Hashtable的类注解可以看到,Hashtable是保留类不建议使用,推荐在单线程环境下使用HashMap替代,如果需要多线程使用则使用ConcurrentHashMap替代。

12. 如何决定使用HashMap还是TreeMap?

对于在Map中插入、删除、定位一个元素这类操作,HashMap是最好的选择,因为相对而言HashMap的插入会更快,但如果你要对一个key集合进行有序的遍历,那TreeMap是更好的选择的

13. 说一下HashMap的实现原理?

HashMap基于Hash算法实现的,我们通过put(key,value)储存,get(key)来获取。当传入Key时,HashMap会根据key.hashCode()计算出hash值,根据hash值将value保存在bucket里。当计算出的hash值相同时,我们称之为hash冲突,HashMap的做法是用链表和红黑树储存相同hash值的value。当hash冲突的个数比较少时,使用链表否则使用红黑树。

补充:

  1. 注意在多线程的时候不要使用HashMap,要使用ConcurrentHashMap来代替(主要是在其扩容机制)

  2. 在JDK1.8之前采用的是头插法(容易造成扩容死循环),在JDK1.8之后采用的是尾插法。

  3. hashmap的储存数据说起,默认的hashmap大小是16,当数据量过大的时候,毫无疑问,hashmap需要扩容去支持存放更多的数据

我们先来看源码:(JDK1.7)
public V put(K key, V value) {
  // 判断key是否为空
        if (key == null)
          // 若为空返回一个null
            return putForNullKey(value);
  // 计算hash值
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
  // 遍历节点中的数据
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
  
        modCount++;
  // 若key不存在,新增一个节点
        addEntry(hash, key, value, i);
        return null;
    }
3.1 putForNullKey():

当key为null的时候自动转向putForNullKey()方法,用来处理null键,将它们放到table[0]的位置,下面的是putForNullKey(),用来遍历entry链表,如果遍历后,发现没有key为null,则直接添加一个entry,键为null,值为新的value;当遍历后,发现key有null的时候,就返回值,让新值覆盖旧值

private V putForNullKey(V value) {
            for (Entry<K,V> e = table[0]; e != null; e = e.next) {
                if (e.key == null) {
                    V oldValue = e.value;
                    e.value = value;
                    e.recordAccess(this);
                    return oldValue;
                }
            }
            modCount++;
            addEntry(0, null, value, 0);
            return null;
        }
3.2 indexFor():

它没有对hash表的长度取余而使用了位运算符来得到索引,这是为什么呢?因为length在hashmap中默认为2的幂次方,所以length-1所的的二进制都是1构成的,所以hash和这个length-1做位运算符其实也是对hash表的长度取余。位运算符快于四则运算。(这是为什么要去size为2的幂次方的原因,因为如果取其他数值,hash碰撞的几率增大,可能size的二进制某一位上事0,导致好几个table位置无法存放数据,造成空间浪费)

static int indexFor(int h, int length) {
    return h & (length-1);
}
3.3 比较校验

找到新元素的table[index]后,就遍历该位置上的entry链;如果仅仅使用equals进行链表上比较效率会很低,所以我们先使用hash来比较过滤,遇到相同的值就覆盖,返回旧值。如果没有相同值就直接addEntry()进行头插法插入链表

3.4 addentry

增加entry方法addEntry():addEntry的源码:判断当前table的size大于等于边界值并且index位置上不为空,就两倍扩容table他的大小,然后再根据传入的hash,key,value重新定向这个元素的位置。创建entry并创建

void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
 
        createEntry(hash, key, value, bucketIndex);
    }
3.5 resize

扩容resize(扩容条件,当前容量大于边界值,并且添加的index对应的table上不为null)

void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
 
        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
3.6 transfer

关键事空表扩容后的拷贝到新表的方法transfer方法,下面是源码:将table上的entry进行遍历,传入的rehash用来判断是否用来重新hash,为true的话就判断key是否为null,如果为null就hash值为0,不为null就重hash,然后重定位index,然后头插法放到对应的位置上

void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }
3.7 createEntry()

最后进行createEntry(),头插法插入,最后将e传入到新的table[]中,表示指针指向旧值

oid createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

14. 说一下HashSet的实现原理?

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

15. ArrayList和LinkedList的区别是什么?

  • 数据结构实现:ArrayList是动态数组的数据结构实现,而LinkedList是双向链表的数据结构实现
  • 随即访问效率:ArrayList比LinkedList在随机访问的时候效率高,因为LinkedList是线性的数据储存方式,所以需要移动指针从前往后依次查找。
  • 增加和删除效率:在非首尾的增加和删除操作,LinkedList要比ArrayList效率高,因为ArrayList增删操作要影响数组内的其他数据的下标

综合来说,在需要频繁取集合中的元素时,更推荐使用ArrayList,而在插入和删除操作较多时,更推荐使用LinkedList。

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

  • 线程安全:Vector使用Synchronized来实现线程同步,是线程安全的,而ArrayList是非线程安全的
  • 性能:ArrayList在性能方面要优于Vector。
  • ArrayList和Vector都会根据实际的需要动态的调整容量,只不过在Vector扩容每次会增加1倍,而ArrayList只会增加50%

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

  • 相同点:都是返回第一个元素,并在队列中删除返回的对象
  • 不同点:如果没有元素,poll()会返回null,而remove()会直接抛出NoSuchElementException异常

代码实例:

Queue<String> queue = new LinkedList<String>();

queue. offer("string"); // add

System. out. println(queue. poll());

System. out. println(queue. remove());

System. out. println(queue. size());

18. 那些集合类线程是安全的

Vector、Hashtable、Stack是线程安全的,而像HashMap、则是非线程安全的,不过在JDK1.5之后随着Java.util.concurrent并发包的出现,他们也有了自己对应线程安全类,比如HashMap对应的线程安全类就是ConcurrentHashMap。

19.迭代器Iterator怎么使用?有什么特点?

iterator使用代码如下:

List<String> list = new ArrayList<>();

        list.add("CodeWorld");

        list.add("FC");

        Iterator<String> iterator = list.iterator();

        while (iterator.hasNext()){

            String next = iterator.next();

            System.out.println(next);
        }

Iterator的特点是更加安全、因为它可以确保,在当前遍历的集合元素被更改的时候,就会抛出ConcurrentModificationException 异常

20.Iterator和ListIterator有什么区别?

Iterator 可以遍历Set和List集合,而ListIterator只能遍历List
Iterator只能单向遍历,而ListIterator可以双向遍历(向前/向后遍历)
ListIterator从Iterator接口继承,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置

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

可以使用Collections.unmodifiableCollection(Collection c)方法来创建一个只读集合,这样改变集合的任何操作都会抛出Java. lang. UnsupportedOperationException异常
实例代码如下:
  List<String> list = new ArrayList<>();

        list.add("code");

        Collection<String> strings = Collections.unmodifiableCollection(list);

        strings.add("world");

        System.out.println(list.size());

22. 并行和并发有什么区别

并行:多个处理器或多核处理器同时处理多个任务

并发:多个任务在同一个CPU核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行。

23.守护线程是什么?

守护线程是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件,在Java中垃圾回收线程就是特殊的守护线程

24.创建线程有那几种方式

继承Thread重写run方法
实现Runnable接口
实现Callable接口

25.线程有哪些状态

NEW 尚未启动
RUNNABLE 正在执行中
BLOCKED 阻塞的(被同步锁或者IO锁阻塞)
WAITING 永久等待状态
TIMED_WAITING 等待指定的时间重新被唤醒的状态
TREMINATED 执行完成

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

类的不同:sleep()来自Threed,wait()来自Object
释放锁:sleep()不释放锁;wait()释放锁
用法不同:sleep()时间到会自动恢复;wait()可以使用notify()/notifyAll直接唤醒

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

notifyAll会唤醒所有的线程,notify()唤醒一个线程。notifyAll()调用后,会将全部线程有等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而notify()只会唤醒一个进程,具体唤醒哪一个线程有虚拟机控制。

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

创建线程池有七种方式,最核心的是最后一种

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

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

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

4.newSingleThreadScheduledExcutor():创建单线程池,返回ScheduledExcutorService,可以进行定时或周期性的工作调度;

5.newScheduledThreadPool(int corePoolSize):和newSingleThreadScheduledExcutor()类似,创建的ScheduledExcutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程;

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

7.ThreadPoolExecutor():最原始的线程池创建,上面1-3创建方式都是对ThreadPoolExecutor的封装

29.线程池有哪些状态

RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。
SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。
STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行的线程。
TIDYING:所有的任务都销毁了。workcount为0,线程池的状态在转换为TIDYING状态时,会执行钩子方法terminated()。
TERMINATED:terminated()方法结束后,线程池的状态就会变成这个。

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

execute():只能执行Runnable类型的任务
submit():可以执行Runnable和Callable类型的任务

Callable类型的任务可以获取执行的返回值,而Runnable执行无返回值

31.在Java程序中怎么保证多线程安全?

方法一:使用安全类,比如Java.util.concurrent下的类
方法二:使用自动锁synchronized
方法三:使用手动锁

手动锁Java实例代码如下:

Lock lock = new ReentrantLock();

        lock.lock();

        try {

            System.out.println("获取锁");

        }catch (Exception e){

            e.printStackTrace();

        }finally {

            System.out.println("释放锁");

            lock.unlock();
        }

32.多线程中synchronized锁升级的原理是什么?

synchronized锁升级原理:在锁对象的对象头里面有一个threadid字段,在第一次访问的时候threadid为空,jvm让其持有偏向锁,并将threadid设置为其线程id,再次进入的时候会先判断threadid是否与其线程id一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了synchronized锁的升级

锁升级的目的是:锁升级是为了减低了锁带来的性能消耗。在Java6之后优化synchronized的实现方式,使用了偏向锁升级为轻量级锁再次升级为重量级锁的方式,从而减低了锁带来的性能消耗。

33.什么是死锁

当线程A持有独占锁a,并尝试去获取独占锁b的同时,线程B持有独占锁b,并尝试获取独占锁a的情况下,就会发生AB连个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。

34.怎么防止死锁

尽量使用tryLock(Long timeout, TimeUnit unit)的方法(ReentrantLock、ReentrantReadWriteLock),设置超时时间,超时可以退出防止死锁。
尽量使用Java.unit.concurrent并发类代替自己手写锁。
尽量降低锁的使用粒度,尽量不要和几个功能用同一把锁。
尽量减少同步的代码块

35.ThreadLocal是什么?有哪些应用场景?

TheadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立的改变自己的副本,而不会影响其他线程所对应的副本。
ThreadLocal的经典使用场景是数据库连接和session管理等

36.说一下synchronized底层实现原理

synchronized是由一对monitorenter/monitorexit指令实现的,monitor对象是同步的基本实现单元。在Java6之前,monitor的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态带内核态的切换,所以同步操作是一个无差别的重量级操作,性能也很低。但在Java6的时候,Java虚拟机对此进行了改进,提供了三种不同的monitor实现,也就是常说的三种不同的锁:偏向锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能

37.synchronized和volatile的区别是什么

volatile是变量修饰符;synchronized是修饰类、方法、代码段
volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。

38.synchronized和Lock有什么区别?

synchronized可以给类、方法、代码块加锁;而Lock只能给代码块加锁
synchronized不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而Lock需要自己加锁和释放锁,如果使用不当没有unloclk()去释放锁就会造成死锁。
通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

39.synchronized和ReentrantLock区别是什么?

synchronized早期的实现比较低效,对比ReentrantLock,大对数场景性能都相差较大,但是在Java6中对synchronized进行了非常多的改进。

主要区别如下:
ReentrantLock使用起来比较灵活,但是必须有释放锁的配合工作
ReentrantLock必须手动获取与释放锁,而synchronized不需要手动释放锁可开启锁
ReentrantLock只适用于代码块锁,而synchronized可用于修饰方法、代码块等
volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

40.说一下atomic的原理?

atomic只要利用CAS(Compare And Wwap)和volatile和native方法来保证原子操作,从而避免synchronized的高开销,执行效率大为提升

41.什么是反射?

反射是在运行状态中,对任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制

42.什么是Java序列化?什么情况下需要序列化?

Java序列化是为了保存各种对象在内存中的状态,并且可以把保存的对象状态再读取出来。
以下情况需要使用Java序列化:
想要把内存中的对象状态保存到一个文件中或者数据库中的时候
想要套接字在网络上传送对象的时候
想要通过RMI(远程方法调用)传输对象的时候

43.动态代理是什么?有哪些应用?

动态代理是运行时动态生成的代理类

动态代理应用有:spring AOP、Hibernate数据查询、测试框架的后端mock、rpc
Java注解对象获取

44.怎么实现的动态代理

JDK远程动态代理和cglib动态代理。JDK原生动态代理是基于接口实现的,而cglib是基于继承当前类的子类实现的。

45.为什么使用克隆

克隆的对象可能包涵一些已经修改过的属性,而new出来的对象的属性都还是初始化时候的值,所以当需要一个新的对象来保存当前对象的"状态"就靠克隆方法了

46.如何实现对象克隆

实现Cloneable接口重写Object类中的clone()方法
实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆

47.深拷贝和浅拷贝区别是什么?

浅克隆:当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员变量并没有被复制。
深克隆:除了对象本身没有被复制外,对象所包含的所有成员变量也将复制

48.如何防止SQL注入

使用预处理PreparedStatement
使用正则表达式过滤掉字符中的特殊字符

49.TCP为什么是三次握手,两次不行吗?

如果采用两次握手,那么只要服务器发送确认数据包就会建立连接,但由于客户端此时并未响应服务器的请求,那次是服务端就会一直等待客户端,这样服务端就白白浪费了一定的资源。若采用三次握手,服务端没有收到来自客户端的再次确认,则就会知道客户端并没有要求建立请求,就不会浪费服务器的资源。

50.说一下TCP粘包是怎么产生的?

TCP粘包可能会发生在发送端或者接接收端。分别来看两端各种产生粘包的原因:
发送端粘包:发送端需要等缓冲区满才发送出去,造成粘包
接收方粘包:接收方不及时接收缓冲区的包,造成多个包接收

51.说一下你熟悉的设计模式

单例模式:保证被创建一次,节省系统开销
工厂模式:(简单工厂、抽象工厂):解耦代码
观察者模式:定义了对象之间的一对多的依赖,这样一来,当一个对象改变时,它的所有依赖着都会收到通知并自动更新
外观模式:提供一个统一的接口,用来访问子系统中的一群接口,外观定义了一个高层接口,让子系统更容易使用。
模板方法模式:定义了一个算法的骨架,而将一些步骤延迟到子类中,模板方法使得子类可以在不改变算法结构的情况下,重新定义算法的步骤
状态模式:允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类

52.简单工厂和抽象工厂有什么区别?

简单工厂:用来生产同一等级结构中的任意产品,对于新增新的产品,无能为力
工厂方法:用来生产同一等级结构中的固定产品,支持增加新的产品
抽象工厂:用来生产不同产品族的全部产品,对于新增新的商品,无能为力。支持增加新的产品族