目录
TCP和UDP的区别
get和post的区别
Cookie和session的区别
Java的基本类型有哪些?
抽象类和接口区别?
对于堆栈的理解
==和equals区别
如何理解Java多态?
创建线程都有哪些方式
脏读、不可重复度、幻读都是什么?
Java的垃圾回收机制
为什么TCP要三次握手,两次不行吗
为什么hashmap的扩容因子是0.75
hashmap1.7和1.8的扩容机制的不同
介绍一下concurrenthashmap
TCP和UDP的区别
TCP(传输控制协议)和UDP(用户数据报协议)是两种不同的传输层协议,用于在计算机网络中传输数据。它们之间的主要区别如下:
-
连接性:
- TCP是一种面向连接的协议,它在数据传输之前先建立连接,确保数据的可靠性和有序性。它使用三次握手和四次挥手来建立和终止连接。
- UDP是一种无连接的协议,数据包可以直接发送,无需建立连接。这意味着UDP的传输速度更快,但不保证数据的可靠性和顺序性。
-
可靠性:
- TCP提供可靠的数据传输,它确保数据到达目标,并在需要时进行重传。如果数据丢失或损坏,TCP会自动进行恢复。
- UDP不提供可靠性保障,数据包可能会丢失或以不同的顺序到达,应用程序需要自己处理数据的完整性和顺序性。
-
适用场景:
- TCP通常用于需要可靠数据传输的应用,如网页浏览、电子邮件、文件传输等,对数据完整性要求高的场景。
- UDP通常用于实时应用,如音频和视频流传输、在线游戏,以及需要低延迟的应用,它可以更快地传输数据,但可以容忍一些数据丢失。
-
头部开销:
- TCP的头部较大,包含了许多控制信息,因此占用的带宽较多。
- UDP的头部较小,因此占用的带宽较少,适用于带宽有限的情况。
总之,TCP和UDP适用于不同的网络应用场景,选择哪种协议取决于应用的要求,是需要可靠传输还是更快的传输速度。
TCP头部较大:TCP协议在每个数据包中包含了相对较多的控制信息,这些信息用于管理和维护TCP连接以确保可靠的数据传输。这些控制信息包括序列号、确认号、窗口大小、校验和等字段。由于这些额外的信息,TCP数据包的头部大小较大,占用了传输带宽的一部分。
get和post的区别
HTTP(Hypertext Transfer Protocol)中的GET和POST是两种常用的请求方法,用于在客户端和服务器之间传输数据。它们之间的主要区别如下:
-
数据传输方式:
- GET: 通过URL将数据附加在请求中,数据以键值对的形式出现在URL的查询字符串中。因此,GET请求将数据暴露在URL中,可见并容易被缓存、书签等记录。
- POST: 将数据包含在请求的主体中,而不是暴露在URL中。这使得POST请求更适合传输敏感数据或大量数据,因为数据不会出现在URL中。
-
数据大小限制:
- GET: 由于数据附加在URL中,GET请求对传输的数据大小有限制,通常受到浏览器和服务器的限制,且数据传输的安全性较低。
- POST: 由于数据包含在请求主体中,POST请求的数据大小限制相对较高,通常可以传输较大的数据量。
-
安全性:
- GET: 因为数据暴露在URL中,GET请求在安全性方面较差,适合传输非敏感数据。
- POST: POST请求将数据包含在请求主体中,因此安全性更高,适合传输敏感数据,如登录凭证或支付信息。
-
可缓存性:
- GET: GET请求通常可以被浏览器缓存,因为它们是幂等的,即多次相同的GET请求应该产生相同的结果。
- POST: POST请求通常不会被浏览器缓存,因为它们可能会引起数据更改或产生不同的结果。
-
幂等性:
- GET: GET请求是幂等的,多次相同的GET请求不应该改变服务器的状态或数据。
- POST: POST请求通常不是幂等的,多次相同的POST请求可能会导致服务器状态的改变或多次提交相同的数据。
总之,GET和POST都用于HTTP通信,但它们在数据传输方式、数据大小限制、安全性、可缓存性和幂等性等方面有不同的特点,应根据具体的需求来选择使用哪种请求方法。GET适用于获取数据,而POST适用于发送数据和进行数据修改。
Cookie和session的区别
Cookie和Session都是用于在Web应用中跟踪用户状态和维护会话信息的机制,但它们之间有一些重要的区别:
-
存储位置:
- Cookie: Cookie是存储在客户端(用户浏览器)的小型文本文件,由服务器发送到客户端并存储在本地。每次浏览器向服务器发送请求时,会自动将相关的Cookie数据附加在请求头中,以便服务器识别用户。
- Session: Session数据通常存储在服务器上,而不是客户端。服务器会为每个会话创建一个唯一的标识符(Session ID),该标识符通常存储在Cookie中,但实际的会话数据存储在服务器上。
-
数据存储:
- Cookie: Cookie通常包含少量的文本数据,用于存储有关用户的小型信息,如用户偏好设置、登录凭证等。Cookie的大小受到浏览器对Cookie大小的限制。
- Session: Session可以存储更大、更复杂的数据,因为会话数据存储在服务器上,而不受Cookie大小的限制。通常,Session用于存储用户登录状态、购物车内容、用户会话信息等。
-
安全性:
- Cookie: Cookie存储在客户端,因此可以被用户查看和修改。虽然可以将Cookie标记为安全和HttpOnly,以增加安全性,但仍然存在被窃取和滥用的风险。
- Session: 由于Session数据存储在服务器上,用户无法直接查看或修改会话数据,这提高了安全性。但服务器上的Session数据也需要受到适当的保护,以防止未经授权的访问。
-
生命周期:
- Cookie: Cookie可以设置过期时间,可以是会话级别(浏览器关闭时失效)或持久性(在一段时间后失效)的。
- Session: Session通常在用户关闭浏览器或一段时间不活动后过期,具体的过期策略可以由开发人员控制。
总之,Cookie和Session都用于在Web应用中管理用户状态和维护会话信息,但它们的存储位置、数据存储、安全性、生命周期等方面有不同的特点,应根据具体的需求和安全考虑选择合适的机制。在实际应用中,它们通常会一起使用,例如,将Session ID 存储在Cookie中,以便服务器识别会话。
Java的基本类型有哪些?
Java的基本数据类型,也称为原始数据类型,用于存储单个值。Java有以下基本数据类型:
-
整数类型:
byte
:8位,范围为-128到127。short
:16位,范围为-32,768到32,767。int
:32位,范围为约-21亿到21亿。long
:64位,范围非常大,约为-9百京到9百京。
在数值上,百京可以理解为10^23(或1后面跟着23个零)
-
浮点数类型:
float
:32位,用于存储小数,精度约为6-7位有效数字。double
:64位,用于存储双精度小数,精度约为15-16位有效数字。
-
字符类型:
char
:16位,用于存储一个字符,如字母、数字、符号等。
-
布尔类型:
boolean
:表示真或假值。
抽象类和接口区别?
抽象类(Abstract Class)和接口(Interface)是面向对象编程中的两种不同的概念,它们用于实现多态性和代码抽象,但它们之间有一些关键的区别:
-
定义方式:
- 抽象类:抽象类使用
abstract
关键字来定义,可以包含抽象方法(即没有实际实现的方法)和具体方法(有实际实现的方法)。 - 接口:接口使用
interface
关键字来定义,它只包含抽象方法(没有方法体),没有具体方法。
比如下面我可以这样定义一个抽象类
让student来继承,并在main方法里面调用
运行结果如下:
- 抽象类:抽象类使用
-
继承关系:
- 抽象类:一个类只能继承一个抽象类,即Java中的单继承。抽象类可以拥有成员变量,构造方法,和非抽象方法。
- 接口:一个类可以实现多个接口,即Java中的多接口实现。接口中只包含抽象方法和常量字段(
final
变量),没有构造方法和具体方法。
-
构造方法:
- 抽象类:可以有构造方法,用于初始化抽象类的成员变量。
- 接口:不能有构造方法,因为接口不能被实例化。
-
成员变量:
- 抽象类:可以包含实例变量(成员变量)。
- 接口:只能包含常量字段,这些字段通常被视为公共常量。
-
实现方式:
- 抽象类:通过继承来实现,子类必须使用
extends
关键字扩展抽象类,并实现抽象类中的抽象方法。 - 接口:通过实现来实现,类使用
implements
关键字来实现接口,并提供接口中定义的所有抽象方法的具体实现。
- 抽象类:通过继承来实现,子类必须使用
-
用途:
- 抽象类:通常用于表示一种基本的抽象概念,可以包含共享的代码和属性。它们常常用于建立类层次结构中的基类。
- 接口:用于定义一组合同(contract),要求实现类提供特定的行为。接口常用于实现多态性,允许一个类实现多个接口。
总之,抽象类和接口都是在Java中实现代码抽象和多态性的重要机制,但它们的使用方式和语法有明显的区别,我们应根据需求和设计目标选择合适的抽象方式。通常,如果需要表示共享代码和属性,使用抽象类;如果需要定义一组规范或行为合同,使用接口。有时也可以同时使用抽象类和接口来实现更复杂的继承结构。
对于堆栈的理解
堆栈(Stack)通常遵循"后进先出"(Last-In, First-Out,LIFO)的原则,即最后进入堆栈的元素是第一个被移除的,而最先进入堆栈的元素是最后被移除的。
以下是关于堆栈的完整描述:
堆栈(Stack)是一种常见的计算机科学数据结构,用于存储和管理数据。堆栈遵循LIFO(Last-In, First-Out)原则,这意味着最后进入堆栈的元素会首先被移除,而最先进入堆栈的元素会最后被移除。
堆栈的基本操作包括:
-
入栈(Push): 当要将一个元素放入堆栈时,这个元素被添加到堆栈的顶部,成为堆栈的新顶部元素。
-
出栈(Pop): 当要移除堆栈顶部的元素时,这个元素被从堆栈中移除,并且下一个元素成为新的堆栈顶部元素。
-
查看栈顶(Top): 可以查看堆栈顶部的元素,但不将其移除。
-
空栈(Empty Stack): 如果堆栈中没有任何元素,它被称为"空栈"。
堆栈的常见应用包括函数调用栈、表达式求值、内存管理和回溯算法等。
==和equals区别
在Java中,==
和 equals()
是用来比较对象的两个不同的方法,它们有以下区别:
-
比较的对象类型:
==
用于比较两个对象的引用(内存地址),即判断两个对象是否是同一个对象。equals()
用于比较两个对象的内容,即判断两个对象是否在逻辑上相等。
-
作用范围:
==
可以用于比较任何两个对象,无论它们的类型是什么。equals()
方法通常需要在类中进行重写,以便根据类的定义来比较对象的内容。因此,equals()
的行为可以根据类的实现而不同。
-
默认行为:
- 默认情况下,
==
比较的是对象的引用,即使两个对象的内容相同,如果它们是不同的实例,则==
也会返回false
。 - 默认情况下,
equals()
方法继承自Object
类,它比较的是对象的引用,而不是内容。因此,如果不在类中进行重写,equals()
方法的行为与==
相同。
我们从Object的equals源码可以看出来,默认情况下,
equals()
比较的是对象的引用,而不是内容: - 默认情况下,
-
自定义比较逻辑:
- 通过在类中重写
equals()
方法,可以自定义对象之间的比较逻辑。这意味着你可以根据对象的内容来判断它们是否相等,而不仅仅是依赖于引用。 - 如果要比较基本数据类型(如
int
、double
等),则可以使用==
,因为它们是值类型,没有引用。
- 通过在类中重写
示例:
String str1 = new String("Hello");
String str2 = new String("Hello");System.out.println(str1 == str2); // false,比较的是引用
System.out.println(str1.equals(str2)); // true,比较的是内容Integer num1 = 5;
Integer num2 = 5;System.out.println(num1 == num2); // true,比较的是引用
System.out.println(num1.equals(num2)); // true,比较的是内容
为什么String比较的不是地址,因为String类重写了equals方法,以下是String类的源码:
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;}
如何理解Java多态?
Java中的多态(Polymorphism)是面向对象编程的一个重要概念,它允许不同类的对象对相同的方法名作出不同的响应。多态性是面向对象编程中的三大特性之一,其他两个是封装和继承。多态性使得你可以通过通用的接口或父类引用来处理不同的子类对象,从而实现了代码的重用和统一接口。
以下是对Java多态的理解和解释:
-
多态的概念: 多态是指同一方法或操作在不同的对象上有不同的表现形式。具体来说,多态允许不同的类实现相同的方法名,但根据对象的类型调用不同类中的具体方法。这种能力使得代码更灵活、可扩展和易维护。
-
多态的实现方式: 多态性主要通过方法的重写(Override)和方法的重载(Overload)来实现。
- 方法重写(Override): 子类可以重写父类的方法,提供自己的实现。当父类的引用指向子类的对象时,调用相同的方法名将执行子类的方法。
- 方法重载(Overload): 在同一个类中,可以定义多个具有相同名称但参数列表不同的方法。编译器根据方法参数的类型和数量来选择调用合适的方法。
-
多态的应用: 多态性使得代码更具灵活性和可维护性。它常常应用于以下场景:
- 方法的统一接口: 多态允许不同的类实现相同的接口或抽象类,并以一致的方式调用这些方法。
- 代码重用: 多态性促进了代码的重用,因为可以使用通用的父类引用来处理不同的子类对象。
- 运行时绑定: 多态允许在运行时确定对象的实际类型,并调用相应的方法,这称为运行时多态或动态绑定。
创建线程都有哪些方式
在Java中,有多种方式可以创建线程,以下是常见的创建线程的方式:
1. 继承Thread
类:
- 创建一个继承自
Thread
类的子类。 - 重写
run()
方法,在其中定义线程的执行逻辑。 - 创建子类的实例并调用
start()
方法启动线程。
class MyThread extends Thread {public void run() {// 线程的执行逻辑}
}MyThread myThread = new MyThread();
myThread.start();
2. 实现Runnable
接口:
- 创建一个实现
Runnable
接口的类。 - 实现
run()
方法,在其中定义线程的执行逻辑。 - 创建实现类的实例,并将其作为参数传递给
Thread
类的构造方法。 - 调用
start()
方法启动线程。
class MyRunnable implements Runnable {public void run() {// 线程的执行逻辑}
}MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
3. 使用匿名内部类:
- 可以通过匿名内部类的方式创建线程。
- 直接在
Thread
类的构造方法中传入一个Runnable
匿名内部类。
Thread thread = new Thread(new Runnable() {public void run() {// 线程的执行逻辑}
});
thread.start();
4. 使用Executor
框架:
- 使用
java.util.concurrent.Executor
接口及其实现类来创建和管理线程池。 - 可以提交
Runnable
任务给线程池执行。
Executor executor = Executors.newFixedThreadPool(2);
executor.execute(new Runnable() {public void run() {// 线程的执行逻辑}
});
脏读、不可重复度、幻读都是什么?
详情见我这一篇博客:MySQL的事务隔离级别_谦虚的荆南芒果的博客-CSDN博客
Java的垃圾回收机制
详情见我这一篇博客:深入探讨Java虚拟机(JVM):执行流程、内存管理和垃圾回收机制_谦虚的荆南芒果的博客-CSDN博客
为什么TCP要三次握手,两次不行吗
TCP(传输控制协议)使用三次握手来建立一个可靠的连接,而不是两次握手,主要是为了确保双方都能够正常通信并同步初始序列号。以下是为什么需要三次握手的原因:
确认双方都准备好: 在进行数据传输之前,需要确保客户端和服务器都准备好建立连接。如果只有两次握手,存在这样的情况:客户端发送连接请求,但由于某种原因,请求没有及时到达服务器,而客户端误认为连接已建立。如果服务器随后发送数据,客户端将无法识别这些数据,因为它并不知道连接是否成功建立。三次握手可以确保双方都准备好了才会建立连接。
防止旧连接的问题: 如果只有两次握手,可能会导致某些网络延迟的数据包在旧连接已经关闭的情况下到达服务器。服务器可能会误认为这些数据包属于新连接,从而引发问题。通过三次握手,服务器可以确保旧连接已经完全关闭,避免了这种混淆。
防止重复连接的问题: 两次握手可能导致一个已经关闭的连接在某些情况下重新建立,这可能会导致不必要的资源浪费。三次握手可以降低这种情况发生的可能性。
下面是三次握手的基本流程:
- 客户端向服务器发送一个连接请求(SYN)。
- 服务器接收到请求并确认客户端的连接请求(ACK)。
- 客户端接收到服务器的确认后,再次向服务器发送确认(ACK)。
这个过程确保了双方都知道对方已经准备好了,而且序列号已经同步,可以开始进行数据传输。如果只有两次握手,就无法达到这种双向确认的效果,可能会导致连接不稳定或不可靠。因此,TCP采用了三次握手来确保连接的可靠性和正确性。
为什么hashmap的扩容因子是0.75
HashMap 的扩容因子通常设置为 0.75 的原因是为了在平衡内存占用和性能之间找到一个合适的权衡点。这个扩容因子决定了 HashMap 在什么时候进行扩容,以确保 HashMap 中的元素数量不会太接近数组的容量,从而保持较好的性能。
如果扩容因子设置得太小,例如 0.5,那么 HashMap 会更频繁地进行扩容,因为容易达到容量的上限,这会增加额外的内存开销和性能损耗。另一方面,如果扩容因子设置得太大,例如 1.0,HashMap 将会在容量接近饱和时才进行扩容,这可能导致哈希冲突增多,性能下降。
0.75 这个值是在平衡内存和性能之间找到的一种妥协。它在一定程度上减少了扩容的频率,同时又不会让 HashMap 的装载因子太高,从而保持了较好的性能和内存使用效率。这个值经验性地选择,通常在实际应用中表现良好。当 HashMap 中的元素数量达到容量的 75% 时,HashMap 就会自动扩容,以保持较好的性能表现。
hashmap1.7和1.8的扩容机制的不同
在 Java 的 HashMap 中,版本 1.7 和 1.8 之间确实存在一些不同之处,尤其是在扩容机制方面。以下是这两个版本之间的主要区别:
HashMap 1.7 的扩容机制:
扩容触发条件: 在 HashMap 1.7 中,扩容是在达到阈值时触发的,这个阈值是根据容量(数组大小)和装载因子(默认为 0.75)计算的。当元素数量达到容量乘以装载因子时,HashMap 会进行扩容操作。
扩容方式: 在 1.7 版本中,扩容是通过创建一个新的数组,将原有的键值对重新分配到新数组中来完成的。这个过程会比较耗时,因为需要重新计算哈希值,并将键值对移动到新的位置。
扩容时机: 在 1.7 版本中,扩容会在 put 操作时触发,而不是在初始化 HashMap 时就分配一块固定大小的数组空间。
HashMap 1.8 的扩容机制:
扩容触发条件: HashMap 1.8 引入了一种新的扩容机制,称为树化(Treeify)。当桶中的链表长度达到一定阈值(默认为 8),链表将被转换为红黑树,这可以提高在高冲突情况下的查询性能。扩容仍然是在元素数量达到容量乘以装载因子时触发的。
扩容方式: 与 1.7 不同,1.8 版本中的扩容方式更加高效。在扩容时,不再是简单地将键值对重新分配到新数组中,而是在原有的数组上创建一个更大的数组,然后将原有的桶(包括链表或树)分散到新数组的不同位置。这减少了重新计算哈希值的开销,提高了性能。
扩容时机: 与 1.7 不同,1.8 版本中在初始化 HashMap 时就会分配一块固定大小的数组空间,而不是在 put 操作时触发扩容。这可以减少扩容的频率。
综上所述,HashMap 1.8 引入了树化机制和更高效的扩容方式,以提高性能和降低内存占用。这些改进使得 HashMap 在高负载情况下的性能更加稳定。
介绍一下concurrenthashmap
ConcurrentHashMap 是 Java 集合框架中的一个线程安全的哈希表实现。它是设计用来支持多线程并发操作的,因此在多线程环境下使用非常高效,并且提供了比传统的 HashMap 更好的性能。
下面是一些关于 ConcurrentHashMap 的重要特点和信息:
线程安全性: ConcurrentHashMap 是线程安全的,多个线程可以同时读取和修改它,而不需要额外的同步措施。这使得它非常适合在多线程应用程序中使用,特别是在高并发环境中。
分段锁设计: ConcurrentHashMap 的实现采用了分段锁(Segment),内部数据结构被分成多个段(Segment),每个段都拥有自己的锁。这种设计允许多个线程在不同的段上进行操作,从而降低了锁竞争的程度,提高了并发性能。
高性能: ConcurrentHashMap 在高并发场景下表现出色,能够提供比传统的同步哈希表更好的性能。它允许多个线程同时进行读操作,以及一定程度的并发写操作,从而减少了锁的争用。
可伸缩性: 由于 ConcurrentHashMap 的分段锁设计,它可以很好地扩展到多核处理器和大规模多线程应用程序,而不会因为锁争用而导致性能下降。
不允许空键值: 与 HashMap 不同,ConcurrentHashMap 不允许存储键或值为空(null),因为空键会导致无法区分键不存在和键映射到 null 值的情况。
遍历性能: ConcurrentHashMap 提供了高效的遍历操作,包括键集、值集和键值对集的遍历。这些操作在迭代期间不会被阻塞,允许并发读取。
总的来说,ConcurrentHashMap 是一个强大的多线程并发集合,适用于需要高性能且线程安全的哈希表操作的场景。在 Java 并发编程中,它是一个重要的工具,能够帮助开发人员处理复杂的并发访问问题。但需要注意,虽然 ConcurrentHashMap 提供了高效的并发支持,但在一些特定的场景下,仍需要根据实际需求选择合适的集合类型。
希望大家支持,下一期我会整理出来更多面试题!