Java基础问题——八股盛宴 | 3w字分享
目录
面向对象与面向过程的区别?
Java面向对象有哪些特征,如何应用?
介绍下Java中的四种引用?
Java中创建对象有几种方式?
Java中的序列化和反序列化是什么?
什么是Java中不可变类?
Java中的Expection和Error的区别是什么?
异常处理时候应该注意什么?
Java异常的处理方式?
介绍下try-with-resource语法
什么是Java的多态特性?
Java中参数传递是按值还是按引用?
为什么Java不支持多重继承?
Java方法重载和方法重写之间的区别是什么?
什么是Java内部类?它有什么运用?
JDK8新特性有哪些?
Java中Stirng、StringBuffer、StringBuilder的区别是什么?
Java中Stringbuilder是怎么实现的?
Java中包装类型和基本类型的区别是什么?
接口和抽象类有什么区别?
JRE和JDK有什么区别?
你使用过哪些JDK提供的工具?
HashCode的作用?
有没有可能两个不相等的对象有相同的hashcode?
Java中hashCode和equals方法是什么?它们与== 操作符之间有什么区别?
Java中hashCode和equals方法之间有什么关系?
什么是Java中的动态代理?
JDK动态代理和CgLib动态代理有什么区别?
Java中注解原理是什么?
你使用过Java中反射机制吗?如何应用反射?
谈谈你对反射的理解?
什么是Java的SPI机制?
Java泛型的作用是什么?
什么是Java 泛型擦除?
什么是Java泛型的上下限定符?
Java中深拷贝和浅拷贝有什么区别?
什么是Java中Integer缓存池?
Java的类加载过程是怎么样的?
什么是Java中的BigDecimal?
BigDecimal为什么能够保证精度不丢失?
使用new String(“嘿嘿”)语句在Java中会创建多少个对象?
Java中final、finally、finalize各有什么区别?
为什么在Java中编写代码时候会遇到乱码问题?
为什么JDK9中将String的char数组改为byte数组?
如果一个线程在Java中被两次调用start方法,会发生什么?
栈和队列在Java中区别是什么?
Java中Optional类是什么?有什么用?
Java的I/O流是什么?
什么是Java的网络编程?
Java中基本数据类型有哪些?
什么是Java中的自动装箱和拆箱?
什么是Java中的迭代器?
Java运行时异常和编译时异常的区别是什么?
什么是Java中继承机制?
什么是Java的封装特性?
Java中的访问修饰符有哪些?
Java中静态方法和实例方法的区别是什么?
Java中for循环与foreach循环的区别是什么?
说说什么是fail-fast?
什么是Java中双亲委派模型?
Java中wait()和sleep()的区别?
Java和Go的区别?
Java Object类中有什么方法?有什么用?
Java中字节码是什么?
什么是BIO、NIO、AIO?
什么是Channel?
什么是Selector?
八股,怎么说呢。我之前系统学习的内容,进行梳理。通过问题的方式,表达出得当的内容,这件事本身就很难。面试时心态、状态、掌握知识的情况等。关于八股文,我不想有太多死记硬背的内容,更多的是希望自我输出。问题,都可以答,答的怎么样,是否算满分答案?很难界定。答的内容不宜过长,答的角度不宜偏离自我含糊的点。我在想,八股文的问题,作为正式面试前的预演,也是对旧知识的回顾。贵在积累,重在坚持。
文章中可能涉及大量的超链接,将过往的博客内容联系起来。
不要急,不要慌,稳扎稳打。
2025/4/24日 暖夜于天软
-
面向对象与面向过程的区别?
面向过程: 分析解决问题的步骤,然后用函数将这些步骤一一实现,在使用的时候,一一调用即可。性能较高。单片机、嵌入式开发等一般采用的就是面向过程开发。
面向对象:将构成问题的事务分解成若干个对象,而建立对象的目的也不是为了完成一个个步骤,而是为了描述清楚某个事务在解决问题过程中所发生的行为。面向对象有封装、继承、多态的特性,所以易回复、易复用、易拓展。我们可以设计低耦合、高内聚的系统,但性能上来说,比面向过程差。
-
Java面向对象有哪些特征,如何应用?
面向对象编程是什么,三大特性。
细聊三大特性
面向对象编程是利用类和对象编程的一种思想。
万物可归类,类是对于世界事物的高度抽象 ,不同的事物之间有不同的关系 ,一个类自身与外界的封装关系,一个父类和子类的继承关系, 一个类和多个类的多态关系。
万物皆对象,对象是具体的世界事物,面向对象的三大特征——封装、继承和多态。
封装说明一个类行为和属性与其他类的关系,低耦合,高内聚;继承是父类和子类的关系;多态说的是类与类的关系。
封装隐藏了类的内部实现机制,可以在不影响使用的情况下改变类的内部结构,同时也保护了数据。对外界而已它的内部细节是隐藏的,暴露给外界的只是它的访问方法。
-
属性封装:使用者只能通过事先定制好的方法来访问数据,可以方便地加入逻辑控制,限制对属性的 不合理操作
-
方法封装:使用者按照既定的方式调用方法,不必关心方法的内部实现,便于使用; 便于修改,增强 代码的可维护性;
继承是从已有的类中派生出新的类,新的类能吸收已有类的数据属性和行为,并能扩展新的能力。
在本质上是特殊~一般的关系,即常说的is-a关系。子类继承父类,表明子类是一种特殊的父类,并且具有父类所不具有的一些属性或方法。从多种实现类中抽象出一个基类,使其具备多种实现类的共同特性 ,当实现类用extends关键字继承了基类(父类)后,实现类就具备了这些相同的属性。继承的类叫做子类(派生类或者超类),被继承的类叫做父类(或者基类)。比如从猫类、狗类、虎类中可以抽象出一个动物类,具有和猫、狗、虎类的共同特性(吃、跑、叫等)。
Java通过extends关键字来实现继承,父类中通过private定义的变量和方法不会被继承,不能在子类中直接操作父类通过private定义的变量以及方法。
继承避免了对一般类和特殊类之间共同特征进行的重复描述,通过继承可以清晰地表达每一项共同特征所适应的概念范围,在一般类中定义的属性和操作适应于这个类本身以及它以下的每一层特殊类的全部对象。
运用继承原则使得系统模型比较简练也比较清晰。
相比于封装和继承,Java多态是三大特性中比较难的一个,封装和继承最后归结于多态, 多态指的是类和类的关系,两个类由继承关系,存在有方法的重写,故而可以在调用时有父类引用指向子类对象。
多态必备三个要素:继承,重写,父类引用指向子类对象。
-
介绍下Java中的四种引用?
强引用 强引用是平常中使用最多的引用,强引用在程序内存不足(OOM)的时候也不会被回收,使用 方式:
String str = new String("str");
System.out.println(str);
软引用 软引用在程序内存不足时,会被回收,使用方式:
// 注意:wrf这个引用也是强引用,它是指向SoftReference这个对象的,
// 这里的软引用指的是指向new String("str")的引用,也就是SoftReference类中T
SoftReference<String> wrf = new SoftReference<String>(new String("str"));
可用场景: 创建缓存的时候,创建的对象放进缓存中,当内存不足时,JVM就会回收早先创建的对象。 弱引用 弱引用就是只要JVM垃圾回收器发现了它,就会将之回收,使用方式:
WeakReference<String> wrf = new WeakReference<String>(str);
可用场景: Java源码中的 java.util.WeakHashMap 中的 key 就是使用弱引用,我的理解就是,一旦我不需要某个引用,JVM会自动帮我处理它,这样我就不需要做其它操作。 虚引用 虚引用的回收机制跟弱引用差不多,但是它被回收之前,会被放入 ReferenceQueue 中。注意哦,其它引用是被JVM回收后才被传入 ReferenceQueue 中的。由于这个机制,所以虚引用大多被用于引用销毁前的处理工作。还有就是,虚引用创建的时候,必须带有 ReferenceQueue ,使用例子:
PhantomReference<String> prf = new PhantomReference<String>(new String("str"),
new ReferenceQueue<>());
可用场景: 对象销毁前的一些操作,比如说资源释放等。 Object.finalize() 虽然也可以做这类动作,但是这个方式即不安全又低效
上诉所说的几类引用,都是指对象本身的引用,而不是指Reference的四个子类的引用(SoftReference等)。
-
Java中创建对象有几种方式?
new 关键字 平时使用的最多的创建对象方式
User user=new User();
反射方式
使用 newInstance(),但是得处理两个异常 InstantiationException、IllegalAccessException:
User user=User.class.newInstance();
Object object=(Object)Class.forName("java.lang.Object").newInstance()
clone方法
Object对象中的clone方法来完成这个操作
反序列化操作
调用 ObjectInputStream 类的 readObject() 方法。我们反序列化一个对象,JVM 会给我们创建一个单独的对象。JVM 创建对象并不会调用任何构造函数。一个对象实现了 Serializable 接口,就可以把对象写入到文中,并通过读取文件来创建对象。
总结 创建对象的方式关键字:new、反射、clone 拷贝、反序列化。
-
Java中的序列化和反序列化是什么?
序列化:将对象转化为字节流的过程,这样对象就可以通过网络传输、持久化存储或缓存。Java提供了java.io.Serializable,只要实现了这个接口,就可以将对象进行序列化。
反序列化:将字节流重新转化为对象的过程,即从存储中读取数据并重新创建对象。
应用场景:网络传输、远程调用、持久化存储、以及分布式系统中进行数据交换。例如:Redis中存储对象的一个方案是通过字符串存储对象的序列化数据;分布式传递对象时候,如果我们修改了上下文对象,在其他服务识别对象可能会报错。
transient关键字:序列化过程中,一些敏感字段不想被序列化,用这个关键字标记。
-
什么是Java中不可变类?
不可变类是指在创建后其状态(对象的字段)无法被修改的类。一旦对象被创建,它的所有属性都不能被修改。这种类的实例在整个生命周期内保持不变。
经典的不可变类有 String、Integer、LocalDate。
实现不可变类的核心点:final修饰类、私有化变量、不暴露set方法,即使有修改需求也是返回新的一个对象。
关键特性:
-
声明类为final,防止子类继承。
-
类的所有字段都是private和final,确保它们在初始化后不能被更敢,
-
通过构造函数初始化所有字段。
-
不提供任何修改对象状态的方法(如setter方法)。
-
如果类包含可变对象的引用,确保这些引用在对象外部无法被修改。例如getter方法中返回对象的副本(new-个新的对象)来保护可变对象。
不可变类的优点:线程安全、缓存友好、防止状态不一致。缺点:性能问题——不可变对象需要每次状态变化时创建新的对象,这可能导致性能开销,尤其是对于大规模对象或者频繁修改的场景。
-
Java中的Expection和Error的区别是什么?
Exception 和 Error 都是Throwable 类的子类(在Java代码中只有继承了Throwable类的实例才可以被throw或者被catch)它们表示在程序运行时发生的异常或错误情况。
总结来看:Exception表示可以被处理的程序异常,Error表示系统级的不可恢复错误。
详细说明:
-
Exception:是程序中可以处理的异常情况,表示程序逻辑或外部环境中的问题,可以通过代码进行恢复或处理。
常见子类有:IOException、SQLException、NullPointerException、IndexoutOfBoundsException等。
Exception 又分为Checked Exception(编译期异常)和Uncheckedd Exception(运行时异常)。
-
Checked Exception:在编译时必须显式处理(如使用 try-catch块或通过 throws声明抛出)。如 IOException。
-
Unchecked Exception:运行时异常,不需要显式捕获。常见的如NullpointerException、IllegalArgumentException等,继承自RuntimeException。
-
Error:表示严重的错误,通常是JVM层次内系统级的、无法预料的错误,程序无法通过代码进行处理或恢复。例如内存耗尽(OutOfMemoryError)、栈溢出(StackOverflowError)。
Error不应该被程序捕获或处理,因为一般出现这种错误时程序无法继续运行。
-
异常处理时候应该注意什么?
异常处理的性能成本非常高,每个 Java 程序员在开发时都应牢记这句话。创建一个异常非常慢,抛出一个异常又会消耗1~5ms,当一个异常在应用的多个层级之间传递时,会拖累整个应用的性能。
仅在异常情况下使用异常;在可恢复的异常情况下使用异常;尽管使用异常有利于 Java 开发,但是在应用中最好不要捕获太多的调用栈,因为在很多情况下都不需要打印调用栈就知道哪里出错了。因此,异常消息应该提供恰到好处的信息。
-
尽量不要捕获Exception,捕获特定的异常,除了别人可以看懂之外,也可以避免捕获不想捕获的异常;
-
捕获异常之后,需要明确的将异常信息记录到日志中;
-
在可能出现异常的地方尽早捕获,不要在调用了好多个方法之后捕获;
-
只在有必要try catch的地方 try catch;
-
可以使用if/else来判断的就不要用异常,因为异常肯定比条件语句低效;
-
不要在finally中return或者处理返回值。
-
Java异常的处理方式?
Java 通过面向对象的方法进行异常处理,一旦方法抛出异常,系统自动根据该异常对象寻找合适异常处理器(Exception Handler)来处理该异常,把各种不同的异常进行分类,并提供了良好的接口。在 Java 中,每个异常都是一个对象,它是 Throwable 类或其子类的实例。当一个方法出现异常后便抛出一个异常对象,该对象中包含有异常信息,调用这个对象的方法可以捕获到这个异常并可以对其进行处理。Java 的异常处理是通过 5 个关键词来实现的:try、 catch、throw、throws 和 finally。
在Java应用中,异常的处理机制分为声明异常,抛出异常和捕获异常。
throw和throws的区别:
(1)位置不同:
throw:方法内部
throws: 方法的签名处,方法的声明处
(2)内容不同:
throw+异常对象(检查异常,运行时异常)
throws+异常的类型(可以多个类型,用,拼接)
(3)作用不同:
throw:异常出现的源头,制造异常。
throws:在方法的声明处,告诉方法的调用者,这个方法中可能会出现我声明的这些异常。然后调用者对这个异常进行处理:要么自己处理要么再继续向外抛出异常
1.throws声明异常
通常,应该捕获那些知道如何处理的异常,将不知道如何处理的异常继续传递下
去。传递异常可以在方法签名处使用 throws 关键字声明可能会抛出的异常。注意
非检查异常(Error、RuntimeException 或它们的子类)不可使用 throws 关键字来声明要抛出的异常。
一个方法出现编译时异常,就需要 try-catch/ throws 处理,否则会导致编译错误
2.throw抛出异常
如果你觉得解决不了某些异常问题,且不需要调用者处理,那么你可以抛出异常。 throw关键字作用是在方法内部抛出一个Throwable类型的异常。任何Java代码都可以通过throw语句抛出异常。
3.try catch捕获异常
程序通常在运行之前不报错,但是运行后可能会出现某些未知的错误,但是还不想直接抛出到上一级,那么就需要通过try…catch…的形式进行异常捕获,之后根据不同的异常情况来进行相应的处理。如何选择异常类型
-
介绍下try-with-resource语法
try-with-resources 是 JDK 7 中一个新的异常处理机制,它能够很容易地关闭在 try-catch 语句块中使用的资源。所谓的资源(resource)是指在程序完成后,必须关闭的对象。try-with-resources 语句确保了每个资源在语句结束时关闭。所有实现了 java.lang.AutoCloseable 接口(其中,它包括实现了 java.io.Closeable 的所有对象),可以使用作为资源。
public class Demo03 {public static void main(String[] args) {try(Resource res = new Resource()) {res.doSome();} catch(Exception ex) {ex.printStackTrace();}}
}class Resource implements AutoCloseable {void doSome() {System.out.println("do something");}@Overridepublic void close() throws Exception {System.out.println("resource is closed");}
}
-
什么是Java的多态特性?
多态是指同一个接口或父类引用变量可以指向不同的对象实例,并根据实际指向的对象类型执行相应的方法。
它允许同一方法在不同对象上表现出不同的行为,是面向对象编程(OOP)的核心特性之一。
多态的优点:
通过多态,程序可以灵活地处理不同类型的对象,降低代码耦合度,增强系统的可扩展性。新增子类或实现类时,无需修改原有代码,只需通过接口或父类引用调用即可。
编译时多态和运行时多态
编译时多态和运行时多态是面向对象编程中多态性的两种实现方式,它们分别在不同的阶段决定方法的绑定。
编译时多态:通过方法重载实现,在编译时确定方法的调用。
运行时多态:通过方法重写实现,在运行时确定方法的调用。
-
Java中参数传递是按值还是按引用?
在Java中,参数传递只有按值传递,不论是基本类型还是引用类型
-
基本数据类型(如int,char,boolean等):传递的是值的副本,即基本类型的数值本身。因此,对方法参数的任何修改都不会影响原始变量。
-
引用数据类型(如对象引用):传递的是引用的副本,即对象引用的内存地址。因此,方法内可以通过引用修改对象的属性,但不能改变引用本身,使其指向另一个对象。
区别:
-
基本类型:包括int,float,double,char,boolean等,存储在栈内存中。方法中对基本类型参数的操作只会影响传递的副本,原始变量的值不受影响。
-
引用类型:包括所有的对象和数组,引用类型的变量存储的是对象在堆内存中的地址。当引用类型作为参数传递时,传递的是这个地址的副本。因此,方法内的修改可以影响到传入的对象的内容,但不会影响对象引用本身的地址。
-
为什么Java不支持多重继承?
主要是因为多继承会产生菱形继承(也叫钻石继承)问题,Java之父(詹姆斯·高斯林)就是吸取C++他们的教训,因此不支持多继承。
在Java8之前接口是无法定义具体方法实现的,所以即使有多个接口必须子类自己实现,所以并不会发生歧义。
Java8之后出了默认方法(default method),此时不就又出现的多继承的菱形继承问题了?
所以Java强制规定,如果多个接口内有相同的默认方法,子类必须重写这个方法。
-
Java方法重载和方法重写之间的区别是什么?
-
方法重载(Overloading):在同一个类中,允许有多个同名方法,只要它们的参数列表不同(参数个数、类型或顺序)。主要关注方法的签名变化,适用于在同一类中定义不同场景下的行为。
-
方法重写(Overriding):子类在继承父类时,可以重写父类的某个方法(参数列表、方法名必须相同),从而为该方法提供新的实现。主要关注继承关系,用于子类改变父类的方法实现,实现运行时多态性。
-
什么是Java内部类?它有什么运用?
Java内部类是指在一个类的内部定义的类,Java支持多种类型的内部类,包括成员内部类、局部内部类、匿名内部类和静态为部类。内部类可以访问外部类的成员变量和方法,甚至包括私有的成员。
内部类的作用主要包括:
-
封装性:将逻辑相关的类封装在一起,提高类的内聚性。
-
访问外部类成员:内部类可以方便地访问外部类的成员变量和方法,尤其在需要操作外部类对象的场景下非常有用。
-
简化代码:对于只在一个地方使用的小类,内部类能减少冗余代码,简化结构。
-
事件处理:匿名内部类广泛用于实现回调函数或事件监听,简简化了代码结构,特别是对于实现接口或抽象类的场景。
内部类的类型
-
成员内部类:非静态类,作为外部类的一个成员。它可以直接访问外下部类的所有成员,包括私有成员。
-
静态内部类:定义为static,无法访问外部类的非静态成员,只能老访问外部类的静态成员。
-
局部内部类:定义在方法或代码块中的类,仅在该方法或代码块内可可见,通常用于临时的对象构建。
-
匿名内部类:没有类名的内部类,通常用于创建短期使用的类实例,尤其是在接口回调或事件处理时被广泛使用。
内部类 实际上只是编译层面的概念,像语法糖,在经过编译器之后其实内部类就会提升为外部顶级类,和外部类没有任何区别,所以在JVM中是没有内部类这个概念的。
-
JDK8新特性有哪些?
JDK8较为重要和平日里经常被问的特性如下:
-
用元空间替代了永久代
-
引入了Lambda表达式
-
引入了日期类、接口默认方法、静态方法
-
新增Stream流式接口
-
引入Optional类
-
新增了CompletableFuture、StampedLock等并发实现类。
-
Java8修改了HashMap和ConcurrentHashMap的实现。
-
Java中Stirng、StringBuffer、StringBuilder的区别是什么?
它们都是Java中处理字符串的类,区别主要体现在可变性、线程安全性和性能上:
-
String
-
不可变:string是不可变类,字符串一旦创建,其内容无法更改。每次对string进行修改操作(如拼接、截取等),都会创建新的string对象。
-
适合场景:string适用于字符串内容不会频繁变化的场景,例如少量的字符串拼接操作或字符串常量。
-
-
StringBuffer
-
可变:StringBuffer是可变的,可以进行字符串的追加、删除、插入等操作。
-
线程安全:StringBuffer是线程安全的,内部使用了 synchronized关键字来保证多线程环境下的安全性。
-
适合场景:StringBuffer适用于在多线程环境中需要频繁修改字符串的场景。
-
-
StringBuilder
-
可变:stringBuilder也是可变的,提供了与StringBuffer类似的操作接口。
-
非线程安全:StringBuilder不保证线程安全,性能比stringBuffeer更高
-
适合场景:StringBuilder适用于单线程环境中需要大量修改字符串的场景,如高频拼接操作。
-
-
Java中Stringbuilder是怎么实现的?
StringBuilder主要是为了解决String对象的不可变性问题,提供高效动不态的字符串拼接和修改操作。大致需要实现append、insert...等功能。
大致核心实现如下:
-
内部使用字符数组(char[] value)来存储字符序列
-
通过方法如append()、insert()等操作,直接修改内部的字符数纹组,而不会像String那样创建新的对象。
-
每次进行字符串操作时,如果当前容量不足,它会通过扩展数组容量来容纳新的字符,按2倍的容量扩展,以减少扩展次数,提高性能。
-
Java中包装类型和基本类型的区别是什么?
-
基本类型:Java中有8种基本数据类型(int、long、float、double、char、byte、boolean、short),它们是直接存储数值的变量,位于栈上(局部变量在栈上、成员变量在堆上、静态(类)字段在方法区),性能较高,且不支持nu11。
-
包装类型:每个基本类型都有一个对应的包装类型(Integer、Long、Float 、Double、Character、Byte、Boolean、Short)。包装类型是类,存储在堆中,可以用于面向对象编程,并且支持 null。
-
接口和抽象类有什么区别?
接口和抽象类在设计动机上有所不同。抽象类强调复用,接口强调解耦
-
接口的设计是自上而下的。我们知晓某一行为,于是基于这些行为约束定义了接口,一些类需要有这些行为,因此实现对应的接知。
-
抽象类的设计是自下而上的。我们写了很多类,发现它们之间有共性,有很多代码可以复用,因此将公共逻辑封装成一个抽象关美,减少代码完杀。
所谓的自上而下指的是先约定接口,再实现。
而自下而上的是先有一些类,才抽象了共同父类(实战中很多时候都是因为重构才有的抽象)。
-
JRE和JDK有什么区别?
JRE(Java Runtime Environment)指的是Java运行环境,包含了JVM、核心类库和其他支持运行Java程序的文件。
-
JVM(JavaVirtual Machine):执行Java字节码,提供了Java程序的运行环境。
-
核心类库:一组标准的类库(如java.lang、java.util等),供Java星序使用
-
其他文件:如配置文件、库文件等,支持JVM的运行。
JDK(Java DevelopmentKit)可以视为JRE的超集,是用于开发Java程序的完整开发环境,它包含了JRE,以及用于开发、调试和监控lava 应用程序的工具。
-
JRE:JDK包含了完整的JRE,因此它也能运行Java程序。
-
开发工具:如编译器(javac)、调试器(jdb)、打包工具(jar)等,用于开发和管理Java程序。
-
附加库和文件:支持开发、文档生成和其他开发相关的任务。
-
你使用过哪些JDK提供的工具?
JDK 提供的主要工具:
-
javac:Java编译器,负责将Java源代码编译成字节码(.class文女件)
-
java:运行Java应用程序的命令,使用JVM来解释并执行编译服后的字节码文件。
-
javadoc:生成API文档的工具,能够根据源代码中的注释生成HTML林各式的文档。
-
jar:用于创建和管理JAR文件的工具,可以将多个.class文件打包为单一文件,便于分发和管理。
-
jdb:Java调试工具,用于在命令行中调试Java应用程序,支持断点设设置、变量查看等功能。
性能监控和分析工具:
-
jps:Java进程工具,显示所有正在运行的Java进程,便于监控空和诊断。
-
jstack:生成线程堆栈信息的工具,常用于分析死锁和线程问题。
-
jmap:内存映射工具,可以生成堆转储(heap dump)文件,便于内存泄漏分析和垃圾回收优化。
-
jhat:堆分析工具,配合jmap使用,分析生成的堆转储文件,帮助开发者了解内存使用情况。
-
jstat:JVM统计监控工具,实时监控垃圾回收、内存、类加载等信息,帮帮助开发者调优JVM性能。
-
jconsole:图形化的JVM监控工具,可以监控应用程序的内存、线程和类加载情况,常用于监控和调试。
-
jvisualvm:功能强大的性能分析工具,支持堆、线程、GC的详细监控,还提供内存分析和CPU性能分析。
诊断工具:
-
jinfo:用于查看和修改正在运行的JVM参数,便于动态调优和调整JVM行为。
-
jstatd:远程JVM监控工具,可以通过网络远程监控JVM的状态,适合分布式系统中的性能监控。
-
HashCode的作用?
java的集合有两类,一类是List,还有一类是Set。前者有序可重复,后者无序不重复。当我们在set中插入的时候怎么判断是否已经存在该元素呢,可以通过equals方法。但是如果元素太多,用这样的方法就会比较慢。于是有人发明了哈希算法来提高集合中查找元素的效率。 这种方式将集合分成若干个存储区域,每个对象可以计算出一个哈希码,可以将哈希码分组,每组分别对应某个存储区域,根据一个对象的哈希码就可以确定该对象应该存储的那个区域。hashCode方法可以这样理解:它返回的就是根据对象的内存地址换算出的一个值。这样一来,当集合要添加新的元素时,先调用这个元素的hashCode方法,就一下子能定位到它应该放置的物理位置上。如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址。这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次。
-
有没有可能两个不相等的对象有相同的hashcode?
能.在产生hash冲突时,两个不相等的对象就会有相同的 hashcode 值.当hash冲突产生时,一般
有以下几种方式来处理:
-
拉链法:每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链 表,被分配到同一个索引上的多个节点可以用这个单向链表进行存储.
-
开放定址法:一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总 能找到,并将记录存入
-
再哈希:又叫双哈希法,有多个不同的Hash函数.当发生冲突时,使用第二个,第三个….等哈希函数 计算地址,直到无冲突
-
Java中hashCode和equals方法是什么?它们与== 操作符之间有什么区别?
hashCode、equals和 ==都是Java中用于比较对象的三种方式,但是它们的用途和实现还是有挺大区别的
-
hashCode用于散列存储结构中确定对象的存储位置。可用于快速比较两个对象是否不同,因为如果它们的哈希码不同,那么它们肯定不相等
-
equals用于比较两个对象的内容是否相等,通常需要重写自定义比较逻辑
-
== 用于比较两个引用是否指向同一个对象(即内存地址)。对于基本数据类型,比较它们的值。
在Java中,hashCode
方法和equals方法之间有一个"合约":
-
如果两个对象根据equals方法被认为是相等的,那么它们必须具具有相同的哈希码。
-
如果两个对象具有相同的哈希码,它们并不一定相等,但会被放在同一个哈希桶中。
-
Java中hashCode和equals方法之间有什么关系?
在Java中,hashCode()和equals()方法的关系主要体现在集合类(如HashMap、HashMap、Haskhset)中
它俩决定了对象的逻辑相等性和哈希存储方式。
-
equals()方法:用于判断两个对象是否相等。默认实现是使用 - 比较对象的内存地址,但可以在类中重写equals()来定义自己的相等逻辑。
-
hashCode()方法:返回对象的哈希值,主要用于基于哈希的集合(如HashMap、HashSet)。同一个对象每次调用hashCode()必须返回相同的值,且相等的对象。必须有相同的哈希码。
两者的关系: 不是人话
如果两个对象根据equals()相等,它们的hashCode()值必须侗。即a.equals(b)==true,那么a.hashCode()=b.hashCode()必须为true
但是反过来不要求成立:即两个对象的hashCode()相同,不一定equals()相等。
注意:如果违背上述关系会导致在基于哈希的集合中出现错误行为。例如,HashMap可能无法正确存储和查找元素。
-
什么是Java中的动态代理?
Java中的动态代理是一种在运行时创建代理对象的机制。动态代理允许程序在运行时决定代理对象的行为,而不需要在编译时确定。它通过代理模式为对象提供了一种机制,使得可以在不修改目标对象的情况下对其行为进行增强或调整。
代理可以看作是调用目标的一个包装,通常用来在调用真实的目标之前进行一些逻辑处理,消除一些重复的代码。
静态代理指的是我们预先编码好一个代理类,而动态代理指的是运行时生成代理类。
动态代理主要用途
简化代码:通过代理模式,可以减少重复代码,尤其是在横切关注点(如日志记录、事务管理、权限控制等)方面。
增强灵活性:动态代理使得代码更具灵活性和可扩展性,因为代理对象是在运行时生成的,可以动态地改变行为。
实现AOP:动态代理是实现面向切面编程(AOP,Aspect-OrientedProgramming)的基础,可以在方法调用前后插入额外的逻辑。
-
JDK动态代理和CgLib动态代理有什么区别?
JDK动态代理:只能对接口进行代理,不支持对类进行代理。
特点:面向接口的,不需要导入三方依赖的动态代理,可以对多个不同的接口进行增强,通过反射读取注解时,只能读取到接口上的注解。
原理:面向接口,只能对实现类在实现接口中定义的方法进行增强。
CGLIB代理:通过字节码技术动态生成目标类的子类来实现代理,支持对类(非接口)进行代理。
特点:面向父类的动态代理,需要导入第三方依赖
原理:面向父类,底层通过子类继承父类并重写方法的形式实现增强
Java JDK动态代理实现:Proxy类和invocationHandler接口实现。在程序中通过Proxy.newInstance(,,)
CgLib代理:处理类实现MethodIncerepter。Enhance.create()创建出目标代理对象。
net.sf.cglib.proxy.Enhancer – 主要的增强类
net.sf.cglib.proxy.MethodInterceptor – 主要的方法拦截类,它是Callback接口的子接口,需要用户实现
net.sf.cglib.proxy.MethodProxy – JDK的java.lang.reflect.Method类的代理类,可以方便的实现对源对象方法的调用,如使用:
Object o = methodProxy.invokeSuper(proxy, args);//虽然第一个参数是被代理对象,也不会出现死循环的问题。
net.sf.cglib.proxy.MethodInterceptor接口是最通用的回调(callback)类型,它经常被基于代理的AOP用来实现拦截(intercept)方法的调用。这个接口只定义了一个方法
public Object intercept(Object object, java.lang.reflect.Method method,
Object[] args, MethodProxy proxy) throws Throwable;
第一个参数是代理对象,第二和第三个参数分别是拦截的方法和方法的参数。
-
Java中注解原理是什么?
-
注解其实就是一个标记,是一种提供元数据的机制,用于给代码添添加说明信息。可以标记在类上、方法上、属性上等,标记自身也可可以设置一些值。
-
注解本身不影响程序的逻辑执行,但可以通过工具或框架来利用这些信息进行特定的处理,如代码生成、编译时检查、运行时处理等。
注解是特殊的接口。
处理注解:
-
编译时处理:使用 javax.annotation.processing包进行注解处理器的开发。
-
运行时处理:使用反射机制访问注解,通过Class.getAnnotation()或Field.getAnnotation()等方法获取注解信息。
元注解,即注解的注解,如@Retention、@Target、@lnherited(表示注解是否可以被继承)。
注解的三大保留策略:
@Retention:定义注解的保留策略,即注解的有效范围。
-
RetentionPolicy.SOURCE:注解仅在源码中存在,编译时被丢弃。
-
RetentionPolicy.CLASS:注解存在于编译后的.class文件中,但过运行时不可用
-
RetentionPolicy.RUNTIME:注解在运行时可用,可以通过反射机制访问。
Target
@Target:指定注解可以应用于哪些代码元素。
-
ElementType.TYPE:类、接口(包括注解类型)或枚举。
-
ElementType.FIELD:字段(包括枚举常量)。
-
ElementType.METHOD:方法。
-
ElementType.PARAMETER:方法参数。
-
ElementType.CONSTRUCTOR:构造方法。
-
ElementType.LOCAL_VARIABLE:局部变量。
-
ElementType.ANNOTATION_TYPE:注解类型。
-
ElementType.PACKAGE:包。
-
你使用过Java中反射机制吗?如何应用反射?
Java的反射机制是指在运行时获取类的结构信息(如方法、字段、构造函数)并操作对象的一种机制。反射机制提供了在运行时动态创建对象调用方法、访问字段等功能,而无需在编译时知道这些类的具体信息。
Class类:反射机制的核心,通过Class类的实例可以获取类的可以获取类的的各种信息。
反射的主要功能:
-
创建对象:通过Class.newInstance或Constructor.newInstance()创建对象实例。
-
访问字段:使用Field类访问和修改对象的字段。
-
调用方法:使用 Method类调用对象的方法。
-
获取类信息:获取类的名称、父类、接口等信息。
反射机制的优点:
-
可以动态地获取类的信息,不需要在编译时就知道类的信息。
-
可以动态地创建对象,不需要在编译时就知道对象的类型。
-
可以动态地调用对象的属性和方法,在运行时动态地改变对象的行为。
一般在业务编码中不会用到反射,在框架上用的较多,因为很多场景需要要很灵活,不确定目标对象的类型,届时只能通过反射动态获取对象信息,
例如Spring使用反射机制来读取和解析配置文件,从而实现依赖注入和面向切面编程等功能。
这里可以谈JDBC优化:JDBC-ResultSet结果集优化,从反射和元数据下手。
反射的性能考虑:
反射操作相比直接代码调用具有较高的性能开销,因为它涉及到动态解析和方法调用。
所以在性能敏感的场景中,尽量避免频繁使用反射。可以通过缓存反射结果。例如把第一次得到的Method缓存起来,后续就不要再调用Class.getDeclaredMethod也就不需要再次动态加载了,这样就可以避免反射性能问题。
-
谈谈你对反射的理解?
Java反射(Reflection)是Java语言的一个特性,它允许程序在运行时对自身进行检查,并且能够操作类、接口、字段和方法等。反射提供了强大的功能,但也带来了一定的技术难点。
基本原理:
-
类的加载:Java反射始于类的加载。当使用Class.forName()或其他类加载器加载类时,JVM会读取类的字节码文件(.class文件),并将其转化为Class对象,这个对象包含了类的元数据信息,如类名、包名、父类、实现的接口、字段、方法等。
-
获取类的信息:通过Class对象,我们可以获取类的各种信息。例如,使用getMethods()方法获取类的所有公共方法,使用getDeclaredFields()方法获取类的所有字段(包括私有字段)。
-
动态调用:反射不仅允许我们获取类的信息,还允许我们动态地创建对象、调用方法、修改字段值等。通过newInstance()方法可以创建类的实例,通过getMethod()获取方法并使用invoke()方法调用它。
技术难点:
-
性能:反射操作相比直接操作代码更慢,因为反射涉及到动态解析和类型检查等。
-
安全性:反射允许访问类的私有成员,这可能导致安全漏洞。如果反射被恶意代码使用,可能会破坏系统的安全性。
-
复杂性:反射操作相对复杂,需要深入理解Java的类型系统和类加载机制。
安全措施:
-
访问控制:Java提供了访问控制修饰符(如private、protected和public)来控制对类的成员的访问。虽然反射可以突破这些限制,但我们应该避免在不需要时这样做。在设计API时,应该只暴露必要的公共接口,并隐藏敏感的内部实现。
-
代码签名和验证:JVM在加载类时会对类的字节码进行验证,以确保其符合Java语言的规范。此外,还可以使用代码签名来验证类的来源是否可信。这可以防止恶意代码被加载到JVM中。
-
最小权限原则:在编写使用反射的代码时,应遵循最小权限原则。即只请求执行所需任务所需的最小权限。例如,如果只需要读取某个字段的值,就不要请求修改该字段的权限。
-
安全管理器:Java提供了一个安全管理器(SecurityManager)类,它允许应用程序定义自己的安全策略。通过实现自定义的安全管理器,可以限制反射的使用,例如禁止加载来自不受信任的源的类。
-
代码审计和测试:对使用反射的代码进行严格的审计和测试是确保安全性的重要步骤。这包括检查是否有不安全的反射调用、验证输入数据的有效性以及确保代码符合安全最佳实践等。
-
什么是Java的SPI机制?
接口和实现方已写好就是API接口 和 调用方已写好就是SPI
SPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制。SPI是一种动态替换发现的机制, 比如有个接口,想运行时动态的给它添加实现,你只需要添加一个实现。我们经常遇到的就是java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,mysql和postgresql都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。
-
Java泛型的作用是什么?
Java泛型的作用是通过在编译时检查类型安全,允许程序员编写更通用和灵活的代码,避免在运行时发生类型转换错误。
总结作用:
-
类型安全:泛型允许在编译时进行类型检查,确保在使用集合或或其他泛型类时,不会出现类型不匹配的问题,减少了运行时的ClassCastException错误。
-
代码重用:泛型使代码可以适用于多种不同的类型,减少代码重复,提升可读性和维护性。
-
消除显式类型转换:泛型允许在编译时指定类型参数,从而消除了运行时需要显式类型转换的麻烦。
Java的泛型只在编译时生效,JVM运行时没有泛型。
-
什么是Java 泛型擦除?
泛型擦除指的是Java编译器在编译时将所有泛型信息删除的过程,以确保与Java1.4及之前的版本保持兼容。
泛型参数在运行时会被替换为其上界(通常是Object),这样一来在运行时无法获取泛型的实际类型。
-
作用:泛型擦除确保了Java代码的向后兼容性,但它也限制了在运行时对泛型类型的操作。
-
影响:由于类型擦除,无法在运行时获取泛型的实际类型,也7不能创建泛型类型的数组或对泛型类型使用instanceof检查。
-
什么是Java泛型的上下限定符?
Java泛型的上下界限定符用于对泛型类型参数进行范围限制,主要有。上界限定符(UpperBound Wildcards)和下界限定符(LowerBouind Wildcards)
上界限定符(?extends T):
-
定义: ? extends T 表示通配符类型必须是 T 类型或 T 的子类。
-
作用:允许使用 或其子类型作为泛型参数,通常用于读取操作,确保可以读取为T或T的子类的对象。
public void process(List<? extends Number> list) {Number num = list.get(0); // 读取时是安全的,返回类型是 Number 或其子类// list.add(1); // 编译错误,不能往其中添加元素
}
下界限定符(?super T):
-
定义:?super T表示通配符类型必须是 T 类型或 T 的父类。
-
作用:允许使用 或其父类型作为泛型参数,通常用于写入操作,确保可以安全地向泛型集合中插入 T 类型的对象。
public void addToList(List<? super Integer> list) {list.add(1); // 可以安全地添加 Integer 类型的元素// Integer value = list.get(0); // 编译错误,不能安全地读取
}
-
Java中深拷贝和浅拷贝有什么区别?
-
深拷贝:深拷贝不仅复制对象本身,还递归复制对象中所有引用的对象。这样新对象与原对象完全独立,修改新对象不会影响到原对象。即色包括基本类型和引用类型,堆内的引用对象也会复制一份。
-
浅拷贝:拷贝只复制对象的引用,而不复制引用指向的实际对象。也就是说,浅拷贝创建一个新对象,但它的字段(若是对象类型)指向的是原对象中的相同内存地址。
深拷贝创建的新对象与原对象完全独立,任何一个对象的修改都不会影响另一个。而修改浅拷贝对象中引用类型的字段会影响到原对象,因为为它们共享相同的引用。
-
什么是Java中Integer缓存池?
Java的Integer缓存池(IntegerCache)是为了提升性能和节省内存。根据实践发现大部分的数据操作都集中在值比较小的范围,因此缓存这些对象可以减少内存分配和垃圾回收的负担,提升性能。
在-128到127范围内的 Integer对象会被缓存和复用。
原理:
-
Java在自动装箱时,对于值在-128到127之间的int类型,会直接返回一个已经缓存的Integer对象,而不是创建新的对象。
缓存池的使用场景:
-
自动装箱(Auto-boxing):当基本类型 int转换为包装类 Integer时,若数值在缓存范围内,返回缓存对象。
-
值比较:由于相同范围内的整数使用同一个缓存对象,使用 == 可以正确比较它们的地址(引用相同),而不需要使用equals()。但是要注意意对于超过缓存范围的Integer对象, == 比较的是对象引用,而不是数值。要比较数值,应使用equals()方法。
-
Java的类加载过程是怎么样的?
虚拟机将Class文件加载到内存,并对数据进行校验,转化解析和初始化,形成虚拟机可以直接使用的Java类型,即java.lang.class
-
什么是Java中的BigDecimal?
BigDecimal 是Java中提供的一个用于高精度计算的类,属于java.math包。它提供对浮点数和定点数的精确控制,特别适用于金融和科学计算等需要高精度的领域。
主要特点:
-
高精度:BigDecimal可以处理任意精度的数值,而不像float和double存在精度限制。
-
不可变性:BigDecimal是不可变类,所有的算术运算都会返回新的BigDecimal对象,而不会修改原有对象(所以要注意性能问题)。
-
丰富的功能:提供了加、减、乘、除、取余、舍入、比较等多种方法,并支持各种舍入模式。
BigDecimal它并不依赖于底层硬件的二进制浮点运算,而是使用高精度的算法来进行运算,因此不会受到浮点数舍入误差的影响。
-
BigDecimal为什么能够保证精度不丢失?
BigDecimal能够保证精度,是因为它使用了任意精度的整数表示法,而不是浮动的二进制表示。
BigDecimal内部使用两个字段存储数字,一个是整数部分 intval另一个是用来表示小数点的位置scale,避免了浮点数转化过程中中可能的精度丢失。
计算时通过整数计算,再结合小数点位置和设置的精度与舍入行为,控制结果精度,避免了由默认浮点数舍入导致的误差。
-
使用new String(“嘿嘿”)语句在Java中会创建多少个对象?
会创建1或2个字符串对象。
主要有两种情况:
-
如果字符串常量池中不存在字符串对象"嘿嘿"的引用,那么它会在堆上创建两个字符串对象,其中一个字符串对象的引用会被保存在字符串常量池中。
-
如果字符串常量池中已存在字符串对象"嘿嘿"的引用,则只会在堆中创建1个字符串对象"嘿嘿"。
-
Java中final、finally、finalize各有什么区别?
-
final:用于修饰类、方法、和变量,主要用来设计不可变类、确保保类的安全性、优化性能(编译器优化)。
-
类:被final修饰的类不能被继承。
-
方法:被final修饰的方法不能被重写。
-
变量:被final修饰的变量不可重新赋值,常用于定义常量。
-
-
finally:与try-catch语句块结合使用,用于确保无论是否发生异常,finally代码块都会执行。
主要用于释放资源(如关闭文件、数据库连接等),以保证即使发生主异常,资源也会被正确释放。
-
finalize():是0bject类中的方法,允许对象在被垃圾回收前前进行清理操作。
较少使用,通常用于回收非内存资源(如关闭文件或释放外部资源),但不建议依赖于它,因为JVM不保证 finalize()会被及时执行行。
在JDK9之后:finalize()方法已被标记为废弃,因为Java提供了更好的替代方案(如AutoCloseable接口和 try-with-resources语句)。
-
为什么在Java中编写代码时候会遇到乱码问题?
主要原因是字符编码与解码不一致。在Java中,乱码问题常常由字符编确码(比如UTF-8、GBK)和解码过程的不一致引起。如果在编码时使用了一种字符集,而在解码时使用了另一种,字符将无法正确显示,从而出现乱码。
常见的有:
-
默认编码设置问题:Java默认使用操作系统的字符编码,如果程序在不同操作系统上运行且未明确指定编码,就可能导致字符处理时出现着差异,引发乱码。
-
流处理中的编码问题:在文件或网络流处理中,读取或写入字符时没有指定编码格式,可能会默认使用平台编码,造成乱码问题。
-
数据库乱码问题:数据库字符集和应用字符集不匹配,也会导导致从数据库读取的数据出现乱码,特别是存取多字节字符(如中文)时。
-
为什么JDK9中将String的char数组改为byte数组?
主要是为了节省内存空间,提高内存利用率。
在JDK9之前,String类是基于char[]实现的,内部采用UTF-16编码,每个字符占用两个字节。但是,如果当前的字符仅需一个字节的空间,这就造成了浪费。例如一些 Latin-1字符用一个字节即可表示。
因此JDK9做了优化采用byte[]数组来实现,ASCII字符串(单字节手字符)通过byte[]存储,仅需1字节,减小了内存占用。
并引入了coder变量来标识编码方式(Latin-1或UTF-16)。如0果字符串中只包含Latin-1范围内的字符(如ASCII),则使用单字节编码,否则使用UTF-16。这种机制在保持兼容性的同时,又减少了内存占用。
-
如果一个线程在Java中被两次调用start方法,会发生什么?
会报错!因为在Java中,一个线程只能被启动一次!所以尝试第二次调用start()方法时,会抛llegalThreadStateException异常。
这是因为一旦线程已经开始执行,它的状态不能再回到初始状态。线程的生命周期不允许它从终止状态回到可运行状态。
-
栈和队列在Java中区别是什么?
-
栈(Stack):遵循后进先出(LIFO,Last In,First Out)原则。即,最后插入的元素最先被移除。主要操作包括push
和pop。Java中的Stack类实现了这个数据结构。
-
Java中的队列(Queue):遵循先进先出(FIFO,First In,First Out)原则。即,最早插入的元素最先被移除。主要操作包括中的Queue接口(java.util.Queue)提供了此数据结构的实现,如LinkedlList 和PriorityQueue。
使用场景:
-
栈:常用于函数调用、表达式求值、回溯算法(如深度优先搜索)等场景。
-
队列:常用于任务调度、资源管理、数据流处理(如广度优先搜搜索)等场景。
-
Java中Optional类是什么?有什么用?
Optional是Java8引入的一个容器类,用于表示可能为空的值。它通过提供更为清晰的API,来减少程序中出现null的情况,避免Nul1PointerException(空指针指针异常)的发生。
Optional可以包含一个值,也可以为空,从而表示"值存在"或"值不存在"这两种状态。
作用:
-
减少NullPointerException:通过Optional提供的操作方法,避免直接使用null进行空值检查,从而降低空指针异常的风险。
-
提高代码可读性:Optional提供了一套简洁的API,例如isPresent()、ifPresent()和orElse(),可以让代码更具表达性,清晰地展示处理空值的逻辑。
-
Java的I/O流是什么?
Java的I/O(输入/输出)流是用于处理输入和输出数据的类库。通过流,程序可以从各种输入源(如文件、网络)读取数据,或务数据写入目标位置(如文件、控制台)。
I/O流分为两大类:字节流和字符流,分别用于处理字节级和字符级的数据:
-
字节流:处理8位字节数据,适合于处理二进制文件,如图片、视频等等。主要类是InputStream和OutputStream及其子类。
-
字符流:处理16位字符数据,适合于处理文本文件。主要类是Reader和Writer及其子类。
-
什么是Java的网络编程?
这题一般会出现在笔试题中,例如让你手写一个基于Java实现网络通信的代码。
Java的网络编程主要利用java.net包,它提供了用于网络通信的基基本类和接口。
Java网络编程的基本概念:
-
IP地址:用于标识网络中的计算机。
-
端口号:用于标识计算机上的具体应用程序或进程。
-
Socket(套接字):网络通信的基本单位,通过IP地址和端口号点赞
-
协议:网络通信的规则,如TCP(传输控制协议)和UDP(用户女据报协议)。
Java网络编程的核心类:
-
Socket:用于创建客户端套接字。
-
ServerSocket:用于创建服务器套接字。
-
DatagramSocket:用于创建支持UDP协议的套接字。
-
URL:用于处理统一资源定位符。
-
URLConnection:用于读取和写入URL引用的资源。
import java.io.*;
import java.net.*;public class TCPServer {public static void main(String[] args) {try (ServerSocket serverSocket = new ServerSocket(8080)) {System.out.println("Server is listening on port 8080");while (true) {Socket socket = serverSocket.accept();//异步处理,优化可以用线程池new ServerThread(socket).start();}} catch (IOException e) {e.printStackTrace();}}
}class ServerThread extends Thread {private Socket socket;public ServerThread(Socket socket) {this.socket = socket;}public void run() {try (PrintWriter out = new PrintWriter(socket.getOutputStream(), true);BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {// 读取客户端消息String message = in.readLine();System.out.println("Received: " + message);// 响应客户端out.println("Hello, client!");} catch (IOException e) {e.printStackTrace();}}
}
import java.io.*;
import java.net.*;public class TCPClient {public static void main(String[] args) {try (Socket socket = new Socket("localhost", 8080);PrintWriter out = new PrintWriter(socket.getOutputStream(), true);BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {// 发送消息给服务器out.println("Hello, server!");// 接收服务器的响应String response = in.readLine();System.out.println("Server response: " + response);} catch (IOException e) {e.printStackTrace();}}
}
-
Java中基本数据类型有哪些?
Java提供了8种基本数据类型(PrimitiveTypes),用于处理不同类型的值:
整型:
-
byte:占用1字节(8位),取值范围为-128到127。
-
short:占用2字节(16位),取值范围为-32,768到32,767。
-
int:占用4字节(32位),取值范围为-2^31到2^31-1。
-
long:占用8字节(64位),取值范围为-2^63到2^63-1。
浮点型:
-
float:占用4字节(32位),符合IEEE 754单精度标准。
-
double:占用8字节(64位),符合IEEE 754双精度标准。
字符型:
-
char:占用2字节(16位),存储单个Unicode字符,取值范围为0到65,535。
布尔型:
-
boolean:用于表示true或false两个值,具体存储大小依赖于虚拟机实现。
需要注意:
-
int是基本数据类型,Integer是int的封装类,是引用类型。int默认值是0,而Integer默认值是null,所以Integer能区分出0和null的情况。一旦java看到null,就知道这个引用还没有指向某个对象,再任何引用使用前,必须为其指定一个对象,否则会报错。
-
基本数据类型在声明时系统会自动给它分配空间,而引用类型声明时只是分配了引用空间,必须通过实例化开辟数据空间之后才可以赋值。数组对象也是一个引用对象,将一个数组赋值给另一个数组时只是复制了一个引用,所以通过某一个数组所做的修改在另一个数组中也看的见。虽然定义了boolean这种数据类型,但是只对它提供了非常有限的支持。在Java虚拟机中没有任何供boolean值专用的字节码指令,Java语言表达式所操作的boolean值,在编译之后都使用Java虚拟机中的int数据类型来代替,而boolean数组将会被编码成Java虚拟机的byte数组,每个元素boolean元素占8位。这样我们可以得出boolean类型占了单独使用是4个字节,在数组中又是1个字节。使用int的原因是,对于当下32位的处理器(CPU)来说,一次处理数是32位(这里不是指的是32/64位系统,而是指CPU硬件层面),具有高效存取的特点。
-
什么是Java中的自动装箱和拆箱?
-
自动装箱(Autoboxing):指的是Java编译器自动将基本数据类型转换为它们对应的包装类型。比如,将 int转换为Integer
-
自动拆箱(Unboxing):指的是Java编译器自动将包装类型转换为基本数据类型。比如,将 Integer转换为 int
主要作用:
它在Java5中引入,主要是为了提高代码的可读性,减少手动转换操作,简化了代码编写,开发者可以更方便地在基本类型和包装类型之间进行转换。
常见于:
-
集合类如List<Integer>中无法存储基本类型,通过自动装箱,可以将 int转换为 Integer存入集合。
-
自动装箱和拆箱经常在算术运算中出现,尤其是包装类型参与运算时。
自动装箱和拆箱并不是通过语法糖实现的,它是通过调用包装类型的valueof()和xxxValue()方法实现的。
-
什么是Java中的迭代器?
Iterator是Java集合框架中用于遍历集合元素的接口,允许开发者依次访问集合合中的每一个元素,而不需要关心集合的具体实现。它提供了一种统一的方式来遍历 List、Set等集合类型,通常与 Collection 类接口一起使用。
Iterator的核心方法:
-
hasNext():返回true表示集合中还有下一个元素,返回 false则表表示遍历完毕。
-
next():返回集合中的下一个元素,如果没有更多元素则抛出NoSuchElementException
-
remove():从集合中移除最近一次通过next()方法返回的元素,执行时只能在调用next()之后使用。这个方法是可选的,不是所有的实现都支持该操作。如果不支持,调用时会抛出UnsupportedOperationException。
主要作用:
迭代器使得遍历不同类型的集合更加简洁、统一,避免直接操作索引,提升了代码的可读性和可维护性。
它支持在遍历过程中动态修改集合内容(例如删除元素,这在for-eadh循环中是会报错的)。
List<String> list = Arrays.asList("A", "B", "C");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {String item = iterator.next();System.out.println(item);
}
-
Java运行时异常和编译时异常的区别是什么?
主要有三大区别:分别是发生时机、捕获和处理方式和设计意图。
-
发生时机
-
编译时异常(Checked Exception):发生在编译阶段,编译器会检查此类异常,程序必须对这些异常进行处理(通过try-catch或抛出throws,否则程序将无法通过编译。
-
运行时异常(Unchecked Exception):发生在程序运行期间,编译器不会强制要求处理这些异常。程序员可以选择是否处理它们,通常是程序逻辑错误导致的。
-
-
捕获和处理方式的区别:
-
编译时异常:必须在代码中显式处理,使用 try-catch或者 throws关键字声明抛出。
-
运行时异常:可以不用显式处理,可以选择使用 try-catch捕获处理,或者让程序终止时由JVM抛出。
-
-
设计意图区别:
-
编译时异常:通常是由外部因素引发的异常(如文件I/O操作、数据库连接失败等),开发者无法完全预知这些问题,因此编译器强制要求进行处理
-
运行时异常:一般是由开发者的编程错误或逻辑漏洞引发的,属于程序内部的问题,开发者理论上可以预知,可以在调试阶段发现处理。
-
-
什么是Java中继承机制?
Java中的继承机制是面向对象编程的核心特性之一,允许一个类(子类)继承另一个类(父类)的属性和方法。继承机制使得类之间可以形成层次结构,支持代码重用和扩展。它是实现多态、抽象和代码复用的关键机制。
子类继承父类的字段和方法,可以重用和扩展父类的功能。Java使用extends关键字来表示类的继承关系。
Java支持单继承,即一个类只能直接继承一个父类。子类可以继承父个类的所有公共和受保护的成员,但不能继承父类的私有成员。
子类构造方法首先调用父类的无参构造方法,如果父类没有无参构造方法,子类必须显式调用父类的其他构造方法。
-
什么是Java的封装特性?
Java的封装特性是面向对象编程的核心原则之一,它指的是将对象象的状态(数据)和行为(方法)封装在一个类内部,并通过公开的接口与外部进行交互。封装的主要目的是隐藏对象的内部实现细节,只暴露必要的功能,从而保护数据的完整性和减少系统的复杂性。
-
数据隐藏:通过将类的字段(成员变量)声明为 private或protected,避免直接被外部访问。只有通过类提供的公共方法(如getter和setter)才能访问和修改这些字段。
-
公共接口:通过公共方法(如getter和setter)提供访问对象数据的方式。这样可以对数据进行控制和验证,确保数据的一致性和合法性。
-
保护数据:封装通过限制对数据的直接访问,减少了对对象状态的的不安全修改和潜在的错误。
-
Java中的访问修饰符有哪些?
Java中的访问修饰符用于控制类、字段、方法和构造函数的访问权限。通过使用访问修饰符,可以实现封装,保护数据,并控制不同部分之间的访问范围。
Java主要有以下四种访问修饰符:
-
public:可以被任何类访问。
-
protected:可以被同一包中的其他类访问,也可以被子类(即使子类在不同包中)访问
-
默认(包级别,default):没有显式指定访问修饰符时,默认为包级别。只能被同一包中的其他类访问。
-
private:只能在定义它的类内部访问,外部无法直接访问。
-
Java中静态方法和实例方法的区别是什么?
注意事项
-
静态方法中不能使用this关键字,因为this代表当前对象实例,而静态方法属于类,不属于任何实例。
-
静态方法可以被重载(同类中方法名相同,但参数不同),但不能被子类重写(因为方法绑定在编译时已确定)。实例方法可以被重载,也可以被子类重写
-
实例方法中可以直接调用静态方法和访问静态变量。
-
静态方法不具有多态性,即不支持方法的运行时动态绑定。
Java中类的方法分为类方法(用static修饰)也称为静态方法和实例方法(没有用static修饰)
-
实例方法:
当类的字节码加载到内存中的时候,类的实例方法并没有被分配到入口地址,只有当类的对象创建之后,实例方法才分配了入口地址。从而实例方法可以被类创建的所有的对象所调用,还有一点要注意,当我们创建第一个类的对象时,实例方法的入口地址会完成分配,当后续在创建对象时,不会被分配新的入口地址,该类的所有的对象共享实例方法的入口地址,当该类的所有的对象被销毁,入口的地址才会消失。
-
类方法
当类的字节码文件加载到内存,类方法的入口地址就会被分配完成,所以类方法不仅仅被该类的对象调用,也可以直接通过类名称完成调用,类方法的入口地址只有程序退出才消失。
因为类方法的入口地址的分配要早于实例方法入口地址的分配时间,在oc中类创造的对象不需要release释放掉,这个对象会被放到自动缓冲池中。
-
Java中for循环与foreach循环的区别是什么?
for
for是一种传统的循环结构,允许开发者控制循环的初始化、条件判断和迭代步进。
主要特点:
-
灵活性:可以控制循环的初始值、终止条件和步进方式。可以使用任何条件和任何步进表达式,还可以通过多种变量进行复杂的控制。
-
适用于数组:可以通过索引访问数组的元素。
-
支持修改集合:可以在循环体中修改集合中的元素。
foreach
foreach是Java5引入的一种简化的循环结构,用于遍历数组或实现了Iterable接口的集合。
主要特点:
-
简洁性:语法更简单,减少了初始化、条件检查和更新的样板代码。适合用于遍历数组和实现了Iterable接口的集合。
-
只读访问:不提供对当前索引的访问,因此不适合需要根据索引进行复杂操作的场景。
-
安全性:在遍历过程中不能修改集合的结构(例如,不能在遍历List的同时添加或删除元素),否则会抛出ConcurrentModificationException
-
说说什么是fail-fast?
fail-fast 机制是 Java 集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生 fail-fast 事件。 例如:当某一个线程 A 通过 iterator 去遍历某集合的过程中,若该集合的内容被其他线程所改变了,那么线程 A 访问集合时,就会抛出 ConcurrentModificationException 异常,产生 fail-fast 事件。这里的操作主要是指 add、remove 和 clear,对集合元素个数进行修改。 解决办法:建议使用“java.util.concurrent 包下的类”去取代“java.util 包下的类”。可以这么理解:在遍历之前,把 modCount 记下来 expectModCount,后面 expectModCount 去和 modCount 进行比较,如果不相等了,证明已并发了,被修改了,于是抛出ConcurrentModificationException 异常。
-
什么是Java中双亲委派模型?
双亲委派模型是Java类加载机制的设计模式之一。它的核心思想是:类类加载器在加载某个类时,会先委派给父类加载器去加载,父类加载器天法加载时,才由当前类加载器自行加载。
工作流程:
-
当一个类加载器(如自定义类加载器)试图加载某个类时,先将加载请求向上委派给父类加载器,父类加载器再向上委派给它的父类,直到根类加载器(Bootstrap ClassLoader)。
-
Java中wait()和sleep()的区别?
wait()和sleep()都是用于暂停线程的操作,但它们有明显的区别(先说面试官最关心的):
-
使用要求不同:
-
wait()方法必须在同步块或同步方法内调用,否则会抛出IllegalMonitorstateException。这是因为wait()依赖于对象锁来管理线程的等待和唤醒机制。调用后,当前线程会释放它持有的对象锁,并进入等待状态。
-
sleep()方法可以在任何上下文中调用,不需要获取对象锁。调用后,线程会进入休眠状态,但不会释放它持有的任何锁。
-
-
方法所属类不同:
wait():属于 Object 类。
sleep():属于Thread类。
-
恢复方式不同:
wait():需要被其他线程通过notify()或notifyAll()显式唤醒,或被wait(long timeout)的超时参数唤醒。
sleep():在指定时间后自动恢复运行,或通过抛出 InterrupteedException恢复
-
用途不同:
wait():通常用于线程间通信,配合notify()或notifyAll()来实现线程的协调工作。
sleep():用于让线程暂停执行一段时间,通常用于控制线程的执行频率或模拟延时。
常见错误:
-
误用sleep():有时开发者会错误地使用sleep()进行线程间通信,但是sleep()不释放锁,可能会导致其他线程无法进入同步块,造成线程饥饿或死锁。
-
忽略中断:sleep()可能抛出InterruptedException,如果不正确处理中断信号,可能会寻致线程提前退出或错误行为。
-
Java和Go的区别?
可以从语言设计理念、并发横型、内存管理、生态系统与应用场景来说:
1)语言设计理念:
Java:Java是一种面向对象编程语言,强调继承、多态和封装等OOP特性。它运行在Java虚拟机(JVM)上,实现了"编写一次,到处运行"的跨平台特性。Java的设计目标是建立一个具有高度灵活性和可扩展性的通用编程平台。
Go:Go是一种注重简洁性和高效性的编程语言,主要面向系统级编程利口并发处理。Go强调简单的语法和快速编译,并通过Goroutine和Channel提供了原生的并发支持。Go的设计目标是提高开发者的生产力,并简化构建高性能服务器应用的过程。
2)并发模型:
Java:Java的并发模型基于操作系统线程,使用 Thread类或Executor 框架来管理并发任务。Java并发编程中,通常需要显式地管理线程的创建、同步和资源共享。
Go:Go的并发横型是基于Goroutine的,这是一种比操作系统线程更轻量级的线程。通过Goroutine和Channel,Go实现了轻量级的并发上理,并简化了线程间的通信和同步,
3)内存管理:
Java:Java使用垃圾回收(GC)机制自动管理内存。Java的GC算法种类繁多,开发者可以根据应用需求选择合适的GC策略来优化性能
Go:Go也使用垃圾回收,但设计上更加简洁,专注于减少GC对应用性生能的影响。Go的GC更适合处理大量并发请求,具有较低的暂停时间。
4)生态系统与应用场景:
Java:Java具有庞大的生态系统和丰富的库支持,广泛应用于企业级应用开发、Web开发、大数据处理、Android开发等领域
Go:Go在云计算、微服务、容器化技术(如Docker和Kubernetes)以及高性能服务器开发中得到广泛应用,特别是在需要高并发处理和低延迟的场景中表现突出。
-
Java Object类中有什么方法?有什么用?
-
public boolean equals(Object obj)
作用:用于比较两个对象是否相等。默认实现比较对象的内存地址,即判断两个引用是否指向同一个对象。
使用:通常会重写此方法来比较对象的内容或特定属性,以定义对象的相等性。
-
public int hashCode()
作用:返回对象的哈希码,是对象的整数表示。哈希码用于支持基于哈希的集合(如HashMap和HashSet)。
使用:如果重写了equals方法,则通常也需要重写 hashCode方法,以保证相等的对象具有相同的哈希码。
-
public String toString()
作用:返回对象的字符串表示。默认实现返回对象的类名加上其哈希码的十六进制表示。
使用:通常会重写此方法以提供对象的更有意义的描述。
-
public final Class<?> getClass()
作用:返回对象的运行时类(Class对象)。此方法是Object类中的一个firnal方法,不能被重写
使用:可以用来获取对象的类信息,常用于反射操作。
-
public void notify()
作用:唤醒在对象的监视器上等待的一个线程。该方法需要在同步块或我同步方法中调用。
使用:用于在多线程环境中进行线程间的通信和协调。
-
public void notifyAll()
·作用:唤醒在对象的监视器上等待的所有线程。该方法需要在同步块我同步方法中调用。
使用:与notify()相似,但唤醒所有等待线程,用于处理多个线程之间的协作。
-
public void wait()
作用:使当前线程等待,直到其他线程调用 notify()或 notifyAll()方法。此方法需要在同步块或同步方法中调用
使用:用于线程间的通信,线程会等待直到被唤醒或超时。
-
public void wait(long timeout)
作用:使当前线程等待,直到指定的时间到期或被唤醒。超时后线程会会自动被唤醒
使用:用于实现带有超时的等待机制。
-
public void wait(long timeout, int nanos)
作用:使当前线程等待,直到指定的时间和纳秒数到期或被唤罐
使用:用于实现更精细的等待控制,允许指定等待时间的精确到纳秒。
-
protected Object clone()
作用:创建并返回当前对象的一个副本。默认实现是进行浅拷贝。
使用:通常会重写此方法来实现深拷贝,以确保克隆对象的完整性。
-
protected void finalize()
作用:在垃圾回收器确定不存在对该对象的更多引用时调用,用于进行资源释放等清理工作,
使用:不建议使用,因为它依赖于垃圾回收器的实现,可能会导致不确定的性能问题,推荐使用try-with-resources和AutoCloseable接口进行资源管理。
-
Java中字节码是什么?
Java字节码是Java编译器将Java源代码编译后生成的中间表示形式,位于Java源代码与JVM执行的机器码之间。
Java字节码由JVM解释或即时编译(JIT)为机器码执行。
-
什么是BIO、NIO、AIO?
BIO (Blocking I/O) 、NIO (Non-blocking I/O)、和AIO (Asyncchronous I/O)是三种不同的I/O模型,它们在处理数据传输时的行为有所不同,适用于不同的应用场景:
BIO (Blocking I/O) :
传统的阻塞I/O模式,调用方在发起/0操作时会被阻塞,直到操作完成后才继续执行。适用于连接数较少、逻辑简单的场景。
例如,在Java中,Serversocket 和Socket的accept()方法是阻塞的,需要等待客户端连接。
NIO (Non-blocking I/O) :
非阻塞I/O模式,调用方在发起/0操作后可以立即返回,即使操作未完成成。通常结合I/O多路复用技术(如select poll、epoll),使得一个线程可以同时管理多个连接。
例如,Java中的java.nio包提供了Selector 、Channel等类,可以实现高效的非阻塞I/O。
AIO (Asynchronous I/O) :
异步I/O模式,调用方在发起I/O请求后,不需要等待操作完成。操作系系统或底层库在I/O操作完成后,通过回调或事件通知的方式告知调用方。
例如,Java7引入了java.nio.channels.AsynchronoussocketChannel类,可以实现异步I/O,适用于对响应时间要求较高的应用场景。
-
什么是Channel?
Channel是JavaNIO(New I/O)中的一个核心概念,用于数据的读写操作,它提供了一种比传统流更高效的I/O操作方式:
Channel:
-
是双向的,可以同时支持读取和写入(读/写),与传统的I/O流相比更灵活。传统的流只能单向,要么是输入流要么是输出流
-
常用于非阻塞I/O操作,可以结合Selector来实现多路复用,从而处理多个并发连接。
Channel的种类:
-
SocketChannel:用于基于TCP的网络通信,可以与服务器或客户端进行连接。
-
ServerSocketChannel:用于监听TCP连接,类似于传统I/O中的ServerSocket。
-
DatagramChannel:用于基于UDP的网络通信。
-
FileChannel:用于从文件中读取或向文件中写入数据。
-
什么是Selector?
Selector是JavaNIO(New I/O)中用于实现I/O多路复用的组件,它可可以通过一个单独的线程同时监视多个通道(Channel)的事件。
Selector的作用:
-
管理多个Channel:通过一个Selector实例,程序可以同时监听多个通道的I/O事件(如可读、可写、连接就绪等),从而使一个线程管理多个连接变得高效。
-
非阻塞I/O:Selector通常与非阻塞通道(如SocketChannel)酉配合使用,实现高效的非阻塞I/O操作。它使得程序无需为每个连接创建一个线程,减少了线程的开销。
Selector的事件类型:
OP READ:表示通道中有数据可读。
OPWRITE:表示通道可以向其中写入数据。
OP CONNECT:表示通道完成连接操作。
OPACCEPT:表示通道可以接受新的连接。