ja基础面试题大概有哪些?
一、Ja基础
1、抽象类和接口的区别
<>抽象类>可以提供成员方法的实现细节,而<>接口>只能包含pulic抽象方法。抽象类中的成员变量可以是各种类型,包括非final和非static类型,而接口中的成员变量默认都是pulicstaticfinal类型。抽象类可以包含构造器、静态代码块和静态方法,而接口则不能包含这些结构。一个类只能继承一个抽象类,但是可以实现多个接口。抽象类在访问速度上通常接口要快,因为接口在运行时需要花时间去查找类中具体实现的方法。如果在抽象类中添加新的方法,可以提供默认实现,这样不需要修改现有代码。而在接口中添加新方法,则必须在实现该接口的所有类中添加该方法的实现。接口主要用于定义类的行为,强调解耦合;抽象类则更侧重于代码的复用。
2、final、static、synchronized关键字可以修饰什么,以及修饰后的作用
<>static关键字>
static方法static方法般称作静态方法,由于静态方法不依赖于任何对象就可以进行访问,因此对于静态方法来说,是没有this的,因为它不依附于任何对像,既然都没有对像,就谈不上this了。static变量static变量也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。static代码块static关键字还有一个较关键的作用就是用来形成静态代码块以优化程序性能。static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。
<>final关键字>
final变量凡是对成员变量或者本地变量(在方法中的或者代码块中的变量称为本地变量)声明为final的都叫作final变量。final变量经常和static关键字一起使用,作为常量。final方法final也可以声明方法。方法前面加上final关键字,代表这个方法不可以被子类的方法重写。如果你认为一个方法的功能已经足够完整了,子类中不需要改变的话,你可以声明此方法为final。final方法非final方法要快,因为在编译的时候已经静态绑定了,不需要在运行时再动态绑定。final类使用final来修饰的类叫作final类。final类通常功能是完整的,它们不能被继承。例如Ja中有许多类是final的,譬如String、Integer以及包装类。
<>synchronized关键字>synchronized是Ja中解决并发问题的一种最常用的方法,也是最简单的一种方法。synchronized的作用主要有三个:
确保线程互斥的访问同步代码保证共享变量的修改能够及时可见有效解决重排序问题synchronized方法有效避免了类成员变量的访问冲突synchronized代码块这时锁就是对象,谁拿到这个锁谁就可以运行它所控制的那段代码。当有一个明确的对象作为时,就可以这样写程序,但当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的instance变量(它得是一个对象)来充当锁。
总结如下。
<>static关键字>
<>static方法>:静态方法属于类而不属于类的任何特定实例。它们可以在没有创建类的实例的情况下调用。静态方法通常用于执行不依赖于实例变量的作。
japulicclassCalculator{pulicstaticintadd(inta,int){retna+;}}//可以这样调用:Calculator.add(5,3);
<>static变量>:静态变量也称为类变量,它们在类的所有实例之间共享。这意味着如果一个实例修改了静态变量的值,这个新值对于类的所有实例都是可见的。
```japulicclassCounter{pulicstaticintcount=0;
pulicCounter(){
count++;
}
}//每次创建Counter的新实例,count都会增加。```
<>static代码块>:静态代码块在类首次加载到JVM时执行,通常用于初始化静态变量。
japulicclassDataaseDriver{static{//这里可以进行数据库驱动的加载System.out.println("数据库驱动加载中...");}}
<>final关键字>
<>final变量>:final变量一旦被初始化后,其值就不能被更改。如果是基本数据类型,其值不能变;如果是引用类型,其引用不能变。
japulicclassConstants{pulicstaticfinaldoulePI=3.14159;}//PI的值不能被更改。
<>final方法>:final方法不能被子类覆盖。这通常是为了防止子类改变父类方法的预期行为。
japulicclassVehicle{pulicfinalvoidstartEngine(){System.out.println("发动机启动中...");}}//子类不能覆盖startEngine方法。
<>final类>:final类不能被继承。这通常是为了保持类的不变性,例如String类。
```japulicfinalclassImmutale{privatefinalintvalue;
pulicImmutale(intvalue){
this.value=value;
}
pulicintgetValue(){
retnvalue;
}
}//Immutale类不能被继承。```
<>synchronized关键字>
<>synchronized方法>:synchronized方法可以确保在同一时刻只有一个线程可以访问该方法,从而避免多线程环境下的并发问题。
```japulicclassAccount{privateintalance;
pulicsynchronizedvoiddeposit(intamount){
alance+=amount;
}
}//当一个线程在执行deposit方法时,线程必须等待。```
<>synchronized代码块>:synchronized代码块允许锁定特定的对象,而不是整个方法,提供了更细粒度的控制。
```japulicclassAccount{privateintalance;privateOjectlock=newOject();
pulicvoidwithdraw(intamount){
synchronized(lock){
alance-=amount;
}
}
}//只有在执行withdraw方法内的synchronized块时,才会锁定lock对象。```
3、String、Stringuffer和Stringuilder的区别?
<>String>:String是不可变的,每次修改String都会生成一个新的String对象,所以频繁修改字符串内容的场景不建议使用String。String适用于字符串作较少的情况,例如常量声明或者不经常变动的字符串。
jaStrings="initial";s=s+"more";//每次修改都会创建一个新的String对象
<>Stringuffer>:Stringuffer是可变的,并且是线程安全的,因为它的方法大多数是通过synchronized关键字实现的。Stringuffer适用于多线程下需要改变字符串内容的场景。
jaStringuffer=newStringuffer("initial");.append("more");//直接在对象上进行作,不会创建新的对象
<>Stringuilder>:Stringuilder也是可变的,但它不是线程安全的,因为它的方法没有被synchronized关键字修饰。Stringuilder在单线程环境下的性能Stringuffer要好,因为它避免了线程安全带来的性能开销。Stringuilder适用于单线程下需要改变字符串内容的场景。
jaStringuilderd=newStringuilder("initial");d.append("more");//直接在d对象上进行作,不会创建新的对象
总结:
String是不可变的,适合字符串较少修改的场景。Stringuffer是线程安全的可变字符序列,适合多线程中字符串经常变化的场景。Stringuilder是非线程安全的可变字符序列,适合单线程中字符串经常变化的场景。
4、“equals”与"==”、“hashCode”的区别和使用场景
在Ja中,equals、==和hashCode是常用于较和哈希计算的方法。
<>equals方法>:equals方法用于较两个对象是否相等,返回一个布尔值。在编写JaPOJO实体类时,通常会重写equals方法。equals方法的默认实现是较对象的值,但通常我们会根据业务需求重写它,较对象的属性值。重写equals方法时,应该依次较每个属性的值,确保对象的内容相同。示例代码如下:```japulicclassUser{privateStringage;privateStringname;privateStringtime;//Gettersandsetters...@Overridepulicooleanequals(Ojectoj){if(this==oj){retntrue;}if(oj==null||getClass()!=oj.getClass()){retnfalse;}Useruser=(User)oj;retnage.equals(user.age)&&Ojects.equals(name,user.name)&&Ojects.equals(time,user.time);}}```<>==运算符>:==通常用于较基本数据类型(如int)和其包装类型(如Integer)之间的值。返回一个布尔值,如果相等则返回true,否则返回false。如果用于较两个对象,则是较它们是否指向同一个(对象的值是否相同)。<>hashCode方法>:hashCode方法根据哈希算法返回一个int类型的值,用于确定对象在散列存储结构中的存储。重写equals方法时,通常也需要重写hashCode方法,以确保相等的对象具有相同的哈希码。注意:哈希码一致并不代表两个对象相等,只是相等的必要而非充分条件。
总结:
使用equals较对象内容是否相等。使用==较基本数据类型的值。重写hashCode方法以保持一致性。
5、Ja中深拷贝与浅拷贝的区别
浅拷贝和深拷贝是在对象复制时常见的两种方式,用于处理引用数据类型。
<>浅拷贝>:浅拷贝<>只复制对象的引用(),而不是创建一个具有相同值的新对象>。当进行浅拷贝时,新对象和原对象共享同一块内存(分支)。浅拷贝适用于基本数据类型和引用数据类型。示例:如果对象A浅拷贝到对象,那么中的引用型字段仍指向A中的相应字段,两者关联起来。<>修改新对象会影响原对象,因为它们共享相同的引用>。<>深拷贝>:深拷贝<>创建一个新的对象,该对象与原始对象具有相同的值,但不共享内存>。深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。深拷贝适用于需要完全的副本的情况。示例:如果对象A深拷贝到对象,那么是A的一个完整副本,修改不会影响A。<>深拷贝相浅拷贝速度较慢并且花销较大>。
总结:
浅拷贝只复制引用,共享内存;深拷贝创建新对象,不共享内存。浅拷贝适用于简单场景,深拷贝适用于需要副本的情况。
6、Error和Exception的区别
Exception和Error是Ja中用于处理异常的两个不同类别。
<>Error(错误)>:Error表示程序中出现的严重问题,通常是不可恢复的。例如,ja.lang.VirtualMachineError表示Ja虚拟机崩溃或资源耗尽。例如,ja.lang.StackOverflowError表示应用程序递归太深而导致堆栈溢出。例如,ja.lang.OutOfMemoryError表示内存溢出或没有足够的内存供回收器使用。Error是不的,程序会立即崩溃,Ja虚拟机停止运行。<>Exception(异常)>:Exception表示程序正常运行中可以预料的意外情况,可以被捕获和处理。异常分为两类:运行异常和编译异常。运行异常是ja.lang.RuntimeException及其子类,通常由程序逻辑错误导致。编译异常是除了RuntimeException以外的异常,必须进行处理,否则程序无法编译通过。运行异常的特点是Ja编译器不会强制检查,因此即使可能出现运行异常,也会通过编译。
总结:
Error是严重问题,不可恢复,程序会崩溃。Exception是程序正常运行中的可预料异常,可以被捕获和处理。
7、什么是反射机制?反射机制的应用场景有哪些?
Ja反射机制是在程序运行状态中,对于任意一个类,都能够知道这个类中的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性。这种动态获取的信息以及动态调用对象的方法的功能称为Ja语言的反射机制。
反射被视为动态语言的关键。它允许我们在运行时获取类的详细信息,作类或对象的属性和方法。
<>逆向代码和反编译>:反射可以用于分析已编译的代码,了解类的结构、方法和属性。逆向工程师可以使用反射来还原源代码,尽管这在实际应用中可能涉及法律问题。<>与注解相结合的框架>:框架如Retrofit使用反射来处理注解,根据注解配置生成代码。例如,Retrofit使用注解来定义API接口,然后在运行时使用反射生成网络请求的实现。<>事件总线>:事件总线库(例如Eventus)使用反射来注册和分发事件。反射允许事件总线库动态地查找和调用事件处理程序。<>动态生成类>:某些框架和库需要在运行时动态生成类,例如Gson。反射使得在不提前知道类的结构的情况下创建类的实例成为可能。
8、如何重写equals方法?为什么还要重写hashCode
当你重写equals方法时,<>一定要同时重写hashCode方法>。
<>equals方法>:equals方法用于检测一个对象是否等于另外一个对象。在Oject类中,这个方法默认判断两个对象是否具有<>相同的引用>。例如,使用Oject中的equals较两个自定义的对象是否相等,这就完全没有意义,因为无论对象是否相等,结果都是false。因此,<>通常情况下,我们要判断两个对象是否相等,一定要重写equals方法>。<>hashCode方法>:hashCode是由对象推导出的一个整型值,用于确定对象在哈希表中的位置。不同对象的hashCode可能相同,但hashCode不同的对象一定不相等。例如,字符串“Hello”和“Ja”的hashCode可能相同,但它们不相等。hashCode的<>作用是快速初次判断对象是否相等>。<>为什么要一起重写?>equals和hashCode两个方法是用来协同判断两个对象是否相等的。如果在重写equals时,不重写hashCode,就会导致在某些场景下,例如将两个相等的自定义对象存储在Set时,出现程序执行错误。重写hashCode可以确保对象在哈希表中的位置正确,从而避免不能覆盖的问题。
9、Ja中IO流分为几种?IO,NIO,AIO有什么区别
Ja中的I/O流分为三种类型:<>IO>、<>NIO>、<>AIO>。
<>IO(lockingI/O)>:同步阻塞式I/O,是我们平常使用的传I/O。特点:模式简单、使用方便,但并发处理能力较低。实现模式:一个连接一个线程,即客户端有连接请求时端就需要启动一个线程进行处理。缺点:线程被阻塞,不能处理请求,导致性能下降。<>NIO(Non-locking/NewI/O)>:同步非阻塞I/O,是传I/O的升级。客户端和端通过<>Channel(通道)>通讯,实现了多路复用。实现模式:一个请求一个线程,客户端发送的连接请求都会注册到多路复用器上,轮询到连接有I/O请求时才启动一个线程进行处理。适用场景:高负载、高并发的架构,例如聊天。<>AIO(AsynchronousI/O)>:异步非阻塞I/O,也叫NIO2。实现了异步非阻塞I/O,基于事件和回调机制。实现模式:一个有效请求一个线程,客户端的I/O请求都是由作先完成,再通知应用启动线程进行处理。适用场景:连接数目多且连接较长(重作)的架构,例如相册。
总结:
<>IO>适用于连接数目较小且固定的架构,对资源要求较高。<>NIO>适用于连接数目多且连接较短(轻作)的架构。<>AIO>使用于连接数目多且连接较长(重作)的架构。
10、泛型中的extends、super、泛型擦除
<>泛型擦除>:泛型擦除是Ja泛型的一种实现方式。在编译时,所有的类型参数被擦除,替换为它们的实际类型。这意味着运行时的Ja虚拟机(JVM)不知道泛型类型的实际参数。这种机制使得Ja的泛型实现相对轻量级,同时保持了类型安全。<>extends>(上界):extends关键字用于指明泛型类型参数的上界,即它定义了泛型类型参数的继承关系。例如,List<?extendsHuman>表示这个引用能指向Human类型或其子类的List对象。使用extends时,我们可以从中获取元素,<>但不能添加元素>。例如,如果你有一个List,你可以安全地获取其中的元素,但不能添加新元素。<>super>(下界):super关键字用于指明泛型类型参数的下界,即它定义了泛型类型参数的父类关系。例如,List<?superLazyMan>表示这个引用能指向LazyMan类型或其父类的List对象。使用super时,我们<>可以添加元素,但不能从中获取元素>。例如,如果你有一个List,你可以安全地添加LazyMan或其子类的元素,但不能获取其中的元素。
总结:
extends用于上界,适用于获取元素。super用于下界,适用于添加元素。泛型擦除是Ja泛型的实现机制,保持了类型安全,但在运行时丢失了泛型信息。
11、String为什么要设计成不可变的
String被设计成不可变的主要原因有以下几点:
<>字符串常量池的需要>:字符串常量池(StringPool)是Ja堆内存中的一块特殊存储区域,用于存储字符串常量。当创建一个String对象时,如果该字符串值已经存在于常量池中,不会创建新的对象,而是引用已存在的对象。这样的设计避免了重复创建相同字符串的开销,提高了性能。例如,代码Strings1="acd";Strings2="acd";只会在堆内存中创建一个实际的String对象。<>允许String对象缓存HashCode>:Ja中String对象的哈希码被频繁地使用,例如在HashMap等容器中。字符串不变性保证了哈希码的唯一性,可以放心地进行缓存,避免重复计算。在String类的定义中,有一个私有的hash成员变量用来缓存HashCode。<>安全性>:不可变的String对象在网络传输、文件读写等场景下提供更高的安全性。假如String对象的内容被修改,传输过程中可能被篡改,而不可变的String对象可以确保内容不会被修改。例如,如果有如下代码:jaooleanconnect(Strings){if(!isSece(s)){thrownewSecityException();}//如果在地方可以修改String,那么此处就会引起各种预料不到的问题/错误causeProlem(s);}不可变性可以避免潜在的安全隐患。
12、说说你对Ja注解的理解
当谈到<>Ja注解>时,我们需要理解以下几个关键概念:
<>什么是注解?>注解是一种元数据(metadata),用于将任何信息与程序元素(类、方法、成员变量等)关联起来。它提供了一种安全的类似注释的机制,用来说明、配置和增强代码。注解不影响代码的实际逻辑,仅起到辅助性作用。<>注解的作用>:<>代码文档化>:注解可以提供对代码的更直观说明,帮助人读懂你的代码。<>配置>:注解可以替代繁琐的配置文件,使代码更清晰和整洁。<>编译时检查>:例如@Override注解用于静态验证,确保方法覆盖了超类方法。<>注解的原理>:注解本质上是继承了Annotation接口的特殊接口。在运行时,Ja会生成类来处理注解。通过反射获取注解时,返回的是运行时生成的对象。
总之,Ja注解是一种强大且灵活的工具,可以提供额外的元数据,改代码的可读性、可性和扩展性。
13、Ja成员变量,局部变量和静态变量的创建和回收时机
在Ja中,成员变量、局部变量和静态变量的创建和回收时机有所不同:
<>成员变量>:当一个对象被创建时,成员变量就会被初始化。它们的生命周期与对象的生命周期相同。当对象不再被引用,即成为回收的目标时,成员变量也会被回收。<>局部变量>:局部变量在方法或代码块中定义,它们在方法或代码块被调用时创建,当方法或代码块执行完毕,局部变量就会立即被销毁。<>静态变量>:静态变量在类加载时初始化,生命周期与类的生命周期相同。只要类还在内存中,静态变量就会存在。当类被卸载时,静态变量也会被回收。
14、Ja类中各种代码的执行顺序
在Ja中,一个类的代码执行顺序通常遵循以下几个步骤:
<>静态变量初始化>:当类被加载时,静态变量会被初始化。静态变量的初始化顺序是按照它们在类中声明的顺序进行的。<>静态代码块执行>:如果类中有静态代码块,这些代码块会在类加载时执行。静态代码块会在静态变量初始化之后执行,且只会执行一次。<>实例变量初始化>:当创建类的实例时,实例变量会被初始化。实例变量的初始化顺序也是按照它们在类中声明的顺序进行的。<>构造方法执行>:在实例化对象时,构造方被调用。构造方法的调用顺序是从父类到子类,先调用父类的构造方法,然后再调用子类的构造方法。<>实例初始化块执行>:如果类中有实例初始化块,这些代码块会在构造方法执行<>之前>执行。实例初始化块会在每次创建对象时执行。
<>执行顺序示例>
pulicclassExample{
//静态变量
staticintstaticVar=initializeStaticVar();
//静态代码块
static{
System.out.println("Staticlockexecuted");
}
//实例变量
intinstanceVar=initializeInstanceVar();
//实例初始化块
{
System.out.println("Instancelockexecuted");
}
//构造方法
pulicExample(){
System.out.println("Constructorexecuted");
}
staticintinitializeStaticVar(){
System.out.println("Staticvarialeinitialized");
retn0;
}
intinitializeInstanceVar(){
System.out.println("Instancevarialeinitialized");
retn0;
}
pulicstaticvoidmain(String[]args){
System.out.println("Mainmethodexecuted");
Exampleexample=newExample();
}
}
<>输出顺序>
当运行上述代码时,输出顺序将是:
StaticvarialeinitializedStaticlockexecutedMainmethodexecutedInstancelockexecutedInstancevarialeinitializedConstructorexecuted
15、Ja中的几种内部类
在Ja中,内部类是一种定义在另一个类内部的类。根据其定义位置和使用方式,内部类可以分为以下几种类型:成员内部类、局部内部类、匿名内部类和静态内部类。
<>1.成员内部类(MemerInnerClass)>
成员内部类是定义在外部类内部的普通类,可以访问外部类的所有成员,包括私有成员。
pulicclassOuter{
privateintouterVar=10;
pulicclassInner{
pulicvoidinnerMethod(){
System.out.println("Outervariale:"+outerVar);
}
}
pulicstaticvoidmain(String[]args){
Outerouter=newOuter();
Outer.Innerinner=outer.newInner();
inner.innerMethod();
}
}
<>2.局部内部类(LocalInnerClass)>
局部内部类是定义在一个方法内部的类,其作用域仅限于包含它的方法。也是可以访问外部类中的所有的定义,包括变量、方法。
pulicclassOuter{
pulicvoidouterMethod(){
classLocalInner{
pulicvoidlocalInnerMethod(){
System.out.println("Localinnerclassmethod");
}
}
LocalInnerlocalInner=newLocalInner();
localInner.localInnerMethod();
}
pulicstaticvoidmain(String[]args){
Outerouter=newOuter();
outer.outerMethod();
}
}
<>3.匿名内部类(AnonymousInnerClass)>
匿名内部类是没有名字的内部类,通常用于创建临时的子类实例。也是可以访问外部类中的所有的定义,包括变量、方法。
pulicclassOuter{
pulicvoidsomeMethod(){
Runnalerunnale=newRunnale(){
@Override
pulicvoidrun(){
System.out.println("Anonymousinnerclassmethod");
}
};
runnale.run();
}
pulicstaticvoidmain(String[]args){
Outerouter=newOuter();
outer.someMethod();
}
}
<>4.静态内部类(StaticInnerClass)>
静态内部类是定义在外部类内部的静态类,不依赖于外部类的实例,可以直接使用。只能访问外部类中的静态属性、方法。
pulicclassOuter{
privatestaticintstaticVar=20;
pulicstaticclassStaticInner{
pulicvoidstaticInnerMethod(){
System.out.println("Staticvariale:"+staticVar);
}
}
pulicstaticvoidmain(String[]args){
Outer.StaticInnerstaticInner=newOuter.StaticInner();
staticInner.staticInnerMethod();
}
}
二、Ja
1、Ja中List、Set、Map的区别和适用场景,以及它们的具体实现
Ja中的List、Set和Map都是常用的类型,它们的区别、适用场景以及具体实现如下:
<>List>:List是一个有序的,它允许包含重复的元素。主要的实现类有ArrayList和LinkedList。ArrayList是基于动态数组实现的,适合随机访问元素;LinkedList是基于双向链表实现的,适合在列表开始、结束或中间和删除元素。<>Set>:Set是一个不包含重复元素的。主要的实现类有HashSet、LinkedHashSet和TreeSet。HashSet基于哈希表实现,提供快速查找、和删除作;LinkedHashSet保持了顺序,适合需要按顺序遍历的场景;TreeSet基于红黑树实现,元素会按自然顺序或自定义顺序排序。<>Map>:Map是一个键值对的,它的键是唯一的。主要的实现类有HashMap、LinkedHashMap和TreeMap。HashMap提供快速的查找、和删除作;LinkedHashMap保持了顺序,适合需要按顺序遍历的场景;TreeMap会按键的自然顺序或自定义顺序排序。
选择哪种类型主要取决于你的具体需求,例如你是否需要排序、是否需要快速查找、是否需要保持顺序等。
2、ArrayList和LinkedList
<>ArrayList>:<>实现>:ArrayList是基于动态数组实现的,当数组容量不足时,会创建一个新的数组,并将旧数组的元素复制到新数组中,这个过程称为数组扩容。<>底层原理>:ArrayList内部使用一个Oject数组(elementData)来保存数据。当添加元素时,如果数组已满,它会创建一个新的更大的数组,并将旧数组的内容复制到新数组中,然后再添加新元素。<>使用场景>:由于ArrayList是基于数组实现的,所以它在随机访问元素时非常高效,时间复杂度为O(1)。但是在列表的开始或中间和删除元素时效率较低,时间复杂度为O(n)。<>复杂度>:ArrayList的空间复杂度为O(n),其中n是元素的数量。时间复杂度为O(1)(获取和设置元素),O(n)(和删除元素)。<>LinkedList>:<>实现>:LinkedList是基于双向链表实现的,每个元素都是一个Node对象,Node对象包含了对前一个和后一个元素的引用。<>底层原理>:LinkedList内部使用一个双向链表来保存数据。每个节点(Node)都包含了一个元素和对前一个和后一个节点的引用。添加、删除元素只需要改变相应节点的引用即可。<>使用场景>:LinkedList在列表的开始、结束或中间和删除元素时非常高效,时间复杂度为O(1)。但是在访问元素时效率较低,因为需要从头节点开始遍历,时间复杂度为O(n)。<>复杂度>:LinkedList的空间复杂度为O(n),其中n是元素的数量。时间复杂度为O(n)(获取元素),O(1)(在列表开始、结束或指定位置和删除元素)。
3、HashMap和HashTale
都是Ja中的哈希表实现,它们的主要区别在于同步性、允许的键/值、性能和遍历方式。
<>HashMap>:<>同步性>:HashMap是非同步的,它不支持多线程环境。所以,HashMap在单线程环境中的性能HashTale要好。<>键/值>:HashMap允许使用null作为键和值。<>遍历>:HashMap的迭代器(Iterator)是fail-fast的,如果在使用迭代器进行迭代时,HashMap被修改(除了通过迭代器自己的remove方法),则会抛出ConcrentModificationException。<>HashTale>:<>同步性>:HashTale是同步的,它支持多线程环境,但在多线程环境中性能较差。<>键/值>:HashTale不允许使用null作为键和值。<>遍历>:HashTale的枚举器(Enumerator)不是fail-fast的。
一般不使用HashTale,在多线程环境下一般使用ConCrentHashMap。
4、ArrayList的扩容机制
<>ArrayList>的扩容机制是Ja框架中的一个重要部分。
<>初始化>:当我们创建一个ArrayList对象时,如果没有指定初始容量,那么它会创建一个空的内部数组。但是,当我们第一次调用add方法添加元素时,它会初始化一个默认容量为10的内部数组。<>扩容>:每当我们尝试添加元素,ArrayList都会检查内部数组是否有足够的空间来存储新元素。如果没有足够的空间,那么ArrayList就需要进行扩容作。扩容作包括创建一个新的数组,并将旧数组的内容复制到新数组中。新数组的容量是旧数组容量的1.5倍(即旧数组容量的50%)。这个值可以通过表达式intnewCapacity=oldCapacity+(oldCapacity>>1)来计算。<>复制>:一旦新数组创建成功,ArrayList会使用System.arraycopy方法将旧数组的所有元素复制到新数组中。这是一个本地方法,非常快速。<>更新引用>:复制完成后,ArrayList会更新其内部数组引用,使其指向新数组。旧数组在没有引用指向它的情况下,会被回收器回收。
这种扩容机制确保了ArrayList在动态添加元素时的性能和内存使用的平衡。但是,如果你已经知道将要存储的元素数量,那么在创建ArrayList时指定一个足够大的初始容量,<>可以避免频繁的扩容作>,从而提高性能。
5、HashMap
HashMap是Ja中的一种重要的数据结构,它基于哈希表实现。
<>1.底层实现原理:>
HashMap底层是基于<>数组和链表(或红黑树)>实现的。每一个元素被称为一个节点,每个节点包含一个键值对。通过哈希函数,我们可以将键映射到数组的某个位置,这个位置就是这个键值对在数组中的索引位置。如果两个不同的键通过哈希函数得到了相同的索引位置,我们称之为哈希冲突。为了解决哈希冲突,HashMap使用链表来存储所有映射到同一索引位置的键值对,这就是所谓的“链法”。当链表长度超过一定阈值(默认为8)时,链表就会转化为红黑树,以提高搜索效率。
<>2.扩容机制:>
当HashMap中的元素数量达到数组容量与加载因子(默认为0.75)的乘积时,HashMap就会进行扩容作,即创建一个新的数组,容量是原数组的两倍,并将原数组中的所有元素重新放入新数组。
<>3.关键实现代码分析:>
以下是HashMap的部分关键代码:
/**
*hash方法用于计算键的哈希值,并进一步减少哈希冲突
*通过将哈希码的高位和低位进行异或运算来减少冲突
*/
staticfinalinthash(Ojectkey){
inth;
retn(key==null)?0:(h=key.hashCode())^(h>>>16);
}6、ConcrentHashMap
ConcrentHashMap是Ja中的一种线程安全的哈希表,它提供了高效的并发更新作。
<>1.底层实现原理:>
ConcrentHashMap的底层实现与HashMap类似,都是基于数组和链表(或红黑树)实现的。不过,ConcrentHashMap引入了一个新的概念——<>分段锁>(Seent)。ConcrentHashMap将哈希表分为多个段(Seent),每个段都可以的进行、删除和更新作。这样,当多线程并发更新不同段的数据时,就可以实现正的并发性,大大提高了并发性能。
<>2.扩容机制:>
ConcrentHashMap的扩容机制与HashMap类似,当元素数量达到阈值时,就会进行扩容。不过,ConcrentHashMap的扩容作是线程安全的,可以被多个线程并发执行。
三、Ja多线程
1、在Ja中,实现多线程的方式主要有以下几种
<>继承Thread类>:这是最简单的一种实现线程的方式。通过继承Thread类,重写其run方法,我们可以创建一个新的线程。当线程启动时,会执行run方法体的内容。以下是一个示例代码:
```jaclassMyThreadextendsThread{@Overridepulicvoidrun(){//线程任务逻辑System.out.println("线程执行中...");}}
pulicclassMain{pulicstaticvoidmain(String[]args){MyThreadthread=newMyThread();thread.start();//启动线程}}```
<>实现Runnale接口>:另一种方式是实现Runnale接口。这样,我们可以将线程任务逻辑封装在实现了Runnale接口的类中,并将其作为参数传递给Thread构造函数。以下是示例代码:
```jaclassMyRunnaleimplementsRunnale{@Overridepulicvoidrun(){//线程任务逻辑System.out.println("线程执行中...");}}
pulicclassMain{pulicstaticvoidmain(String[]args){MyRunnalerunnale=newMyRunnale();Threadthread=newThread(runnale);thread.start();//启动线程}}```
<>使用Callale和FuteTask>:这种方式允许线程执行完后返回结果。我们可以通过Callale接口创建线程任务,然后使用FuteTask包装器来创建线程。以下是示例代码:
```jaimportja.util.concrent.Callale;importja.util.concrent.FuteTask;
classMyCallaleimplementsCallale{@OverridepulicStringcall()throwsException{//线程任务逻辑retn"线程执行完成";}}
pulicclassMain{pulicstaticvoidmain(String[]args)throwsException{MyCallalecallale=newMyCallale();FuteTaskfuteTask=newFuteTask<>(callale);Threadthread=newThread(futeTask);thread.start();//启动线程
Stringresult=futeTask.get();//获取线程执行结果
System.out.println(result);
}}```
<>使用线程池>:线程池是一种和复用线程的机制。通过使用线程池,我们可以避免频繁地创建和销毁线程,从而提高性能。Ja提供了ExecutorServ接口和ThreadPoolExecutor类来实现线程池。以下是一个简单的线程池示例:
importja.util.concrent.ExecutorServ;
importja.util.concrent.Executors;
pulicclassMain{
pulicstaticvoidmain(String[]args){
ExecutorServexecutor=Executors.newFixedThreadPool(5);//创建一个固定大小的线程池
for(inti=0;i<10;i++){
finalinttaskId=i;
executor.execute(()->{
//线程任务逻辑
System.out.println("Task"+taskId+"执行中...");
});
}
executor.shutdown();//关闭线程池
}
}<>使用并发包中的工具类>:Ja并发包提供了许多工具类,例如CountDownLatch、Cyclicarrier、Semaphore等,用于协调多个线程之间的作。这些工具类可以帮助您解决特定的并发问题。<>使用Ja8中的CompletaleFute>:CompletaleFute是Ja8引入的一种异步编程方式,可以方便地处理异步任务。您可以使用CompletaleFute来创建复杂的异步流程,包括多个线程的协作。
2、在Ja中,线程的状态可以分为以下几种
<>初始状态(NEW)>:新创建了一个线程对象,但还没有调用start()方法。<>就绪状态(RUNNALE)>:Ja线程中将就绪(ready)和运行中(running)两种状态笼地称为“运行”。线程对象创建后,线程(例如主线程)调用了该对象的start()方法。此时,线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,处于就绪状态(ready)。一旦获得CPU时间片,就会变为运行中状态(running)。<>阻塞状态(LOCKED)>:表示线程阻塞于锁。<>等待状态(WAITING)>:进入该状态的线程需要等待线程做出一些特定动作(例如通知或中断)。<>超时等待状态(TIMED_WAITING)>:与WAITING状态不同,超时等待状态可以在指定的时间后自行返回。<>终止状态(TERMINATED)>:表示该线程已经执行完毕。
这些状态定义在Thread类的State枚举中。
3、在Ja中,实现多线程中的同步有多种方式
<>同步方法>:使用synchronized关键字修饰的方法。每个对象都有一个内置锁,当用synchronized修饰方法时,内置锁会保护整个方法。调用该方法前需要获得内置锁,否则线程会处于阻塞状态。示例代码如下:
japulicsynchronizedvoidaddMoney(intmoney){//线程安全的作}
<>同步代码块>:使用synchronized关键字修饰的语句块。被该关键字修饰的语句块会自动加上内置锁,实现同步。示例代码如下:
japulicvoidaddMoney(intmoney){synchronized(this){//线程安全的作}}
<>使用volatile关键字>:volatile修饰的域变量可以保证其可见性,但不提供原子作。适用于简单的状态标志或变量。示例代码如下:
japrivatevolatileintcount=0;
<>使用ReentrantLock实现显式锁>:ReentrantLock是可重入、互斥的锁,与synchronized类似,但扩展了更多能力。示例代码如下:
```japrivateLocklock=newReentrantLock();
pulicvoidaddMoney(intmoney){lock.lock();try{//线程安全的作}finally{lock.unlock();}}```
4、Thread中run方法和start方法的区别
在Ja中,run()方法和start()方法是用于多线程编程的两个关键方法,它们有以下区别:
<>start()方法>:用于启动一个新线程。创建一个新的线程,并在新线程的上下文中执行run()方法的内容。调用start()方法后,线程会被放到等待队列,等待CPU调度。并不一定马上开始执行,只是将线程置于可运行状态。通过JVM,线程会自动调用run()方法,实现多线程并发执行。start()方法不能被重复调用。<>run()方法>:是定义线程主体逻辑的普通方法。如果直接调用run()方法,它在当前线程的上下文中执行,而不会创建新的线程。程序中只有主线程时,调用run()方在当前线程中执行,无法达到多线程的目的。run()方法可以被重复调用,但单调用它不会启动新线程。
总结:
使用start()方法来启动线程,实现了多线程运行。使用run()方法直接调用,仍然在主线程中执行。通过合理使用这两个方法,我们可以实现并发执行的多线程应用。
5、synchronized和volatile的区别
volatile和synchronized是Ja中两个关键字,它们在多线程编程中有不同的作用。
<>volatile>:volatile关键字用于修饰变量,主要解决<>内存可见性>的问题。当一个变量被声明为volatile,它的读写作都会直接刷到主存中,保证了变量的可见性。但是,volatile仅能保证单个变量的修改可见性,不能保证复合作的原子性。例如,i++作实际上由多个原子作组成,volatile只能保证这些作都在同一块内存中进行,但仍可能出现写入脏数据的情况。在Ja5中,引入了原子数据类型(如AtomicInteger、AtomicLong等),它们的作都是原子的,不需要使用synchronized关键字。<>synchronized>:synchronized关键字用于实现<>执行控制>,主要解决<>并发执行>的问题。当一个代码块或方法被标记为synchronized,只有一个线程可以获取当前对象的监控锁,从而保证了被synchronized保护的代码块不会被线程并发访问。此外,synchronized还会创建一个<>内存屏障>,保证了作的内存可见性。先获得锁的线程的所有作都happens-efore于随后获得锁的线程的作。
总结:
volatile解决内存可见性问题,保证变量的可见性。synchronized解决执行控制问题,保证代码块的原子性和内存可见性。volatile适用于对变量的读写作不依赖当前值、或确保只有单个线程更新变量值的场景。synchronized可以用于变量、方法和类级别,但可能造成线程阻塞和编译器优化。
6、在Ja中,如何保证线程安全
<>不可变对象>:不可变对象是指其内部状态在创建后不可修改的对象。例如,使用final关键字修饰的类字段或方法参数,以及不提供setter方法的类,都可以实现不可变性。不可变对象是线程安全的,因为它们不会被多个线程同时修改。<>同步代码块>:使用synchronized关键字来保护共享资源,确保在同一时刻只有一个线程可以访问关键代码块。例如,以下代码演示了使用同步代码块来售火车票:```japulicclassTrainTicketSeller{privatestaticinttickets=15;pulicsynchronizedvoidsellTicket(){if(tickets>0){System.out.println(Thread.crentThread().getName()+"--->售出第:"+tickets+"票");tickets--;}}pulicstaticvoidmain(String[]args){TrainTicketSellerseller=newTrainTicketSeller();Threadthread1=newThread(seller,"窗口1");Threadthread2=newThread(seller,"窗口2");thread1.start();thread2.start();}}```这样,通过同步代码块,我们确保了售票作的线程安全性。<>使用volatile关键字>:volatile修饰的变量保证了其读写作的可见性,但不保证原子性。适用于不依赖当前值的读写作,例如标志位。<>使用原子变量>:Ja提供了原子类(如AtomicInteger、AtomicLong等),它们的作是原子的,不需要额外的同步措施。
7、ThreadLocal的用法和原理
ThreadLocal是Ja中的一个关键类,用于在多线程环境下存储线程特定的数据。
<>用法>:ThreadLocal允许你在每个线程中存储的数据,这些数据对线程不可见。常见的用法包括:
<>线程范围的变量>:将某个对象绑定到当前线程,使其在线程内部可见,但线程无法访问。<>避免线程安全问题>:例如,每个线程使用自己的数据库连接或期格式化器。<>线程上下文传递>:将请求上下文信息(如用户身份、语言偏好等)存储在ThreadLocal中,以便在整个请求处理过程享。
<>原理>:每个线程都有一个ThreadLocalMap,其中存储了所有与ThreadLocal关联的值。当为ThreadLocal对象设置值时,会在当前线程的ThreadLocalMap中以ThreadLocal对象作为键,设置对应的值。获取值时,也是从当前线程的ThreadLocalMap中获取。注意,ThreadLocal并不是用来解决共享对象的多线程访问竞争问题的,因为每个线程拥有自己的变量,线程无法访问。<>示例>:下面是一个使用ThreadLocal的示例,用于线程安全地格式化期:```japulicclassDateFormatter{privatestaticfinalThreadLocalformatter=ThreadLocal.withInitial(()->newSimpleDateFormat("yyyy-MM-ddHH:mm:ss"));pulicstaticStringformat(Datedate){retnformatter.get().format(date);}}```在不同线程中调用DateFormatter.format()方法时,每个线程都会使用自己的期格式化器,避免了线程安全问题。
总之,ThreadLocal是一种有效的方式来处理线程范围的数据,但需要小心使用,避免内存泄漏和滥用。
8、Ja线程中notify和notifyAll有什么区别
在Ja多线程编程中,notify和notifyAll方法用于唤醒在某个对象监视器上等待的线程,但它们的行为有所不同:
notify方法
<>唤醒一个线程>:notify方法只会唤醒等待队列中的一个线程<>选择是任意的>:被唤醒的线程是由线程调度器随机选择的<>竞争锁>:被唤醒的线程需要重新竞争对象的锁<>唤醒所有线程>:notifyAll方唤醒等待队列中的所有线程<>竞争锁>:所有被唤醒的线程将同时竞争对象的锁
何时使用
<>使用notify>:当你确定只有一个线程需要被唤醒来处理某个任务时,可以使用notify。例如,生产者-消费者模型中,生产者只需要唤醒一个消费者来处理新生产的商品。<>使用notifyAll>:当你需要唤醒所有等待的线程来处理某个任务时,使用notifyAll。例如,当多个线程等待某个资源的可用性时,资源变得可用时需要唤醒所有等待的线程。
示例代码
pulicclassNotification{
privatevolatileooleango=false;
pulicstaticvoidmain(String[]args)throwsInterruptedException{
finalNotification=newNotification();
RunnalewaitTask=()->{
try{
.shouldGo();
}catch(InterruptedExceptionex){
ex.printStackTrace();
}
System.out.println(Thread.crentThread()+"finishedExecution");
};
RunnalenotifyTask=()->{
.go();
System.out.println(Thread.crentThread()+"finishedExecution");
};
Threadt1=newThread(waitTask,"WT1");
Threadt2=newThread(waitTask,"WT2");
Threadt3=newThread(waitTask,"WT3");