基础
&与&&区别
&运算符有两种用法:(1)按位与;(2)逻辑与。
&&运算符是短路与(或简洁与)运算。逻辑与跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端的布尔值都是
true 整个表达式的值才是 true。
&&之所以称为短路运算是因为,如果&&左边的表达式的值是 false,右边的表达式会被直接短路掉,不会进行
运算。很多时候我们可能都需要用&&而不是&,例如在验证用户登录时判定用户名不是 null 而且不是空字符串,应
当写为 username != null &&!username.equals(""),二者的顺序不能交换,更不能用&运算符,因为第一个条件如
果不成立,根本不能进行字符串的 equals 比较,否则会产生 NullPointerException 异常。注意:逻辑或运算符(|)
和短路或运算符(||)的差别也是如此
使用 final 关键字修饰一个变量时,是引用不能变,还是引用的对象不能变?
/*
* 问题:使用final关键字修饰一个变量时,是引用不能变,还是引用的对象不能变
* 答:
* 使用final关键字修饰一个变量时,是指引用变量不能变,引用变量所指向的对象中的内容还是可以改变的。
*/
public class Test10 {
// final修饰基本类型的变量
public static final char CHAR = \'中\';
// final修饰引用类型的变量
public static final StringBuffer a = new StringBuffer("StringBuffer");
public static void main(String[] args) {
// 编译报错,引用不能变
// a = new StringBuffer("hehe");
// 引用变量所指向的对象中的内容还是可以改变的
a.append("xxx");
}
public static int method1(final int i) {
// i = i + 1;// 编译报错,因为final修饰的是基本类型的变量
return i;
}
// 有人在定义方法的参数(引用变量)时,可能想采用如下的形式来阻止方法内部修改传进来的参数对象,
// 实际上,这是办不到的,在该方法内部任然可以增加如下代码来修改参数对象
public static void method2(final StringBuffer buffer) {
buffer.append("buffer");// 编译通过,因为final修饰的是引用类型的变量
}
}
静态变量和实例变量的区别?
语法区别:静态变量需要static关键字修饰,实例变量不需要。
程序运行时的区别:静态变量从属于类,实例变量从属于对象。
实例变量必须创建了实例对象,其中的实例变量才会被分配空间,才能使用这个实例变量;
静态变量即类别量,只要程序加载了类的字节码,静态变量就会被分配空间,即可使用。
综上,实例变量必须创建对象后通过这个对象来使用,静态变量可以直接使用类名来引用。
注意:(static)静态变量的使用也是有局限性的,一个静态方法中不能调用类中的非静态的方法和变量,static修饰的变量在类加载后在内存中只有一份内存空间,可以被一个类的所有实例对象所共享,如:总库100张票,4个窗口卖火车票,卖的都是总库里的票,无论是哪个窗口卖掉的票,总票都会减一。
是否可以从一个 static 方法内部发出对非 static 方法的调用?
不可以。因为非static方法是要与对象关联在一起的,必须创建一个对象后,才可以在该对象上进行方法调用,而static方法调用时不需要创建对象,可以直接调用。也就是说,当一个static方法被调用时,可能还没有创建任何实例 对象,如果从一个static方法中发出对非static方法的调用,那个非static方法是关联到哪个对象上的呢?这个逻辑无法成立,所以,一个static方法内部发出对非static方法的调用。
https://blog.csdn.net/u012110719/article/details/46351691
"=="和 equals 方法究竟有什么区别?
==操作符专门用来比较两个变量的值是否相同,也就是用于比较变量所对应的内存中所存储的数值是否相同,要比较两个 基本类型的数据或两个引用变量是否相等
只能用==操作符。
equals方法用来比较两个独立对象的内容是否相同,就好比去比较两本书是否相同,ta比较的两个对象是独立的:看下面的代码
String a = new String("AA");
String b = new String("AA");
System.out.println(a==b);
System.out.println(a.equals(b));
两条new语句创建了两个对象,然后用a,b两个变量分别指向其中的一个对象,这是两个不同的对象,ta们的首地址是不同的,即a,b中存储的数值是不同的,所以
表达式a==b将返回false,而两个对象中的内容是相同的,所以a,equals(b)返回了true。
注意啊,字符串的比较基本上都是使用equals方法。
如果一个类没有自己定义的equals方法,那么ta将继承Object类的equals方法,Object类的的实现代码如下:
boolean equals(Object o)
{
return this==o;
}
这说明如果一个类没有自己定义的equals方法,ta默认的equals方法,等同于使用==操作符,也就是比较两个变量指向的对象是同一个对象。这时候使用equals和==
会得到相同的结果!!如果希望写的类能够比较两个实例对象的内容是否相同,则需要覆盖equals方法!!
int和Integer的区别
1、Integer是int的包装类,int则是java的一种基本数据类型
2、Integer变量必须实例化后才能使用,而int变量不需要
3、Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int则是直接存储数据值
4、Integer的默认值是null,int的默认值是0
https://www.cnblogs.com/guodongdidi/p/6953217.html
什么是for each循环,它可以循环那些数据类型
增强版的for循环,写法更简洁,更加不容易出错(因为不用担心数组越界),底层也是使用了迭代器获取的,只不过获取迭代器由jvm完成,不需要我们获取迭代器而已,所以在使用foreach循环变量元素的过程中不准使用集合对象对集合的元素个数进行修改。
-
写法:
-
1 for(String it : set){
-
2 System.out.println("集合的元素:" + it);
-
3 }
缺点:
在对数组索引进行操作或者集合进行增删操作时,可能会报错。
https://www.cnblogs.com/shadowdoor/p/6852656.html
重载与重写区别?
方法重载
是让类以统一的方式处理不同类型数据的一种手段。多个同名函数同时存在,具有不同的参数个数/类型。重载Overloading是一个类中多态性的一种表现。
Java的方法重载,就是在类中可以创建多个方法,它们具有相同的名字,但具有不同的参数和不同的定义。
调用方法时通过传递给它们的不同参数个数和参数类型来决定具体使用哪个方法, 这就是多态性。
重载的时候,方法名要一样,但是参数类型和个数不一样,返回值类型可以相同也可以不相同。无法以返回值类型作为重载函数的区分标准。
重写
参数列表必须完全与被重写的方法相同,否则不能称其为重写而是重载。
返回的类型必须一直与被重写的方法的返回类型相同,否则不能称其为重写而是重载。
访问修饰符的限制一定要大于被重写方法的访问修饰符(public>protected>default>private)
重写方法一定不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常。例如:父类的一个方法申明了一个检查异常IOException,在重写这个方法是就不能抛出Exception,只能抛出IOException的子类异常,可以抛出非检查异常。
https://blog.csdn.net/houwanle/article/details/82107161
构造器(constructor)是否可被重写(override)
构造器不能被继承,因此不能被重写,但可以被重载。
接口与抽象类的区别?
1、抽象类和接口都不能直接实例化,如果要实例化,抽象类变量必须指向实现所有抽象方法的子类对象,接口变量必须指向实现所有接口方法的类对象。
2、抽象类要被子类继承,接口要被类实现。
3、接口只能做方法申明,抽象类中可以做方法申明,也可以做方法实现
4、接口里定义的变量只能是公共的静态的常量,抽象类中的变量是普通变量。
5、抽象类里的抽象方法必须全部被子类所实现,如果子类不能全部实现父类抽象方法,那么该子类只能是抽象类。同样,一个实现接口的时候,如不能全部实现接口方法,那么该类也只能为抽象类。
6、抽象方法只能申明,不能实现,接口是设计的结果 ,抽象类是重构的结果
7、抽象类里可以没有抽象方法
8、如果一个类里有抽象方法,那么这个类只能是抽象类
9、抽象方法要被实现,所以不能是静态的,也不能是私有的。
10、接口可继承接口,并可多继承接口,但类只能单根继承。
不允许类多重继承的主要原因是,如果A同时继承B和C,而b和c同时有一个D方法,A如何决定该继承那一个呢?
但接口不存在这样的问题,接口全都是抽象方法继承谁都无所谓,所以接口可以继承多个接口。
https://www.cnblogs.com/yongjiapei/p/5494894.html
final, finally, finalize 的区别
final:java中的关键字,修饰符。
A).如果一个类被声明为final,就意味着它不能再派生出新的子类,不能作为父类被继承。因此,一个类不能同时被声明为abstract抽象类的和final的类。
B).如果将变量或者方法声明为final,可以保证它们在使用中不被改变.
1)被声明为final的变量必须在声明时给定初值,而在以后的引用中只能读取,不可修改。
2)被声明final的方法只能使用,不能重载。
finally:java的一种异常处理机制。
finally是对Java异常处理模型的最佳补充。finally结构使代码总会执行,而不管无异常发生。使用finally可以维护对象的内部状态,并可以清理非内存资源。特别是在关闭数据库连接这方面,如果程序员把数据库连接的close()方法放到finally中,就会大大降低程序出错的几率。
finalize,它是一个方法,属于java.lang.Object类,
它的定义如下:protected void finalize()throws Throwable{}
众所周知,finalize()方法是GC(garbagecollector运行机制的一部分,在此我们只说说finalize()方法的作用是什么呢?
finalize()方法是在GC清理它所从属的对象时被调用的,如果执行它的过程中抛出了无法捕获的异常(uncaughtexception,GC将终止对改对象的清理,并且该异常会被忽略;直到下一次GC开始清理这个对象时,它的finalize()会被再次调用
http://www.cnblogs.com/smart-hwt/p/8257330.html
String、StringBuffer与StringBuilder的区别
String的值是不可变的,这就导致每次对String的操作都会生成新的String对象。
StringBuffer 和 StringBuilder 类的对象能够被多次的修改,并且不产生新的未使用对象
速度快慢为:StringBuilder > StringBuffer > String
StringBuilder 类和 StringBuffer 之间的最大不同在于 StringBuilder 的方法不是线程安全的
https://blog.csdn.net/u011702479/article/details/82262823
所有的类都继承于object类,你用过的object类的直接子类有哪些,object类常用的方法有哪些
- Boolean
- Character
- Class
- ClassLoader
- Compiler
- Enum
- String
- System
- Thread
https://zhidao.baidu.com/question/267505870.html
Object类的常用方法
toString();
equals();
hashCode();
https://www.cnblogs.com/qwddqy/p/7570850.html
什么是泛型,怎么使用的,有什么好处
是一种把类型明确的工作推迟到创建对象或者调用方法的时候才去明确的特殊的类型。
参数化类型,把类型当作参数一样的传递。是一种设计模式,非常好的支持了多态,比如各种容器。
好处:
把运行时期的问题提前到了编译期间,避免了强制类型转换(也就是强转过程中的各种错误,在编译阶段就避免了)
有界泛型也很优秀,参见:
https://blog.csdn.net/archer119/article/details/77923372
java对象序列化为什么要使用serialversionUID
什么要序列化对象
-
- 把对象转换为字节序列的过程称为对象的序列化。
-
- 把字节序列恢复为对象的过程称为对象的反序列化。
对象的序列化主要有两种用途:
1) 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
2) 在网络上传送对象的字节序列。
为什么要使用SerialversionUID
如果用户没有自己声明一个serialVersionUID,接口会默认生成一个serialVersionUID,如果对象发生新增字段,则系统分配的serialversionUID会发生变化,导致反序列化异常。
https://blog.csdn.net/java_mdzy/article/details/78354959
反射
反射的优缺点
优点
反射提高了Java程序的灵活性和扩展性,降低耦合性,提高自适应能力。它允许程序创建和控制任何类的对象,无需提高硬编码目标类
反射是其他一些常用语言,如C、C++、Fortran或者Pascal等不具备的
Java反射技术应用领域很广,如软件测试、JavaBean等
许多流行的开源框架例如Struts、Hibernate、Spring在实现过程中都采用了该技术
https://blog.csdn.net/Elias94/article/details/80361035
https://blog.csdn.net/u010154380/article/details/78150251
缺点
性能第一 Performance Overhead
反射包括了一些动态类型,所以JVM无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被 执行的代码或对性能要求很高的程序中使用反射。
安全限制 Security Restrictions
使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如Applet,那么这就是个问题了。。
内部暴露 Exposure of Internals
由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用--代码有功能上的错误,降低可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。
反射机制的应用场景
- 逆向代码 ,例如反编译
- 与注解相结合的框架 例如Retrofit
- 单纯的反射机制应用框架 例如EventBus 2.x
- 动态生成类框架 例如Gson
java 中有几种类型的流?JDK 为每种类型的流提供了一些抽象类以供继承, 为每种类型的流提供了一些抽象类以供继承,请说出他们分别是哪些类?
多线程部分
https://blog.csdn.net/zuochao_2013/article/details/79860278
什么是多线程?
多线程,是指从软件或者硬件上实现多个线程并发执行的技术。 在一个程序中,这些独立运行的程序片段叫作“线程”,利用它编程的概念就叫作“多线程处理”。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。
主线程就是创建进程中产生的第一个线程,也就是main函数对应的线程。
说一下多线程的好处?
线程的优点
创建一个新线程的代价要比创建一个新进程小的多
线程之间的切换相较于进程之间的切换需要操作系统做的工作很少
线程占用的资源要比进程少很多
能充分利用多处理器的可并行数量
等待慢速 IO操作结束以后,程序可以执行其他的计算任务
计算(CPU)密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
IO密集型应用,为了提高性能,将IO操作重叠,线程可以等待不同的IO操作。
线程的缺点?
性能损失( 一个计算密集型线程是很少被外部事件阻塞的,无法和其他线程共享同一个处理器,当计算密集型的线程的数量比可用的处理器多,那么就有可能有很大的性能损失,这里的性能损失是指增加了额外的同步和调度开销,二可用资源不变。)
健壮性降低(线程之间是缺乏保护性的。在一个多线程程序里,因为时间上分配的细微差距或者是共享了一些不应该共享的变量而造成不良影响的可能影响是很大的。)
缺乏访问控制( 因为进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响 。)
编程难度提高(编写和 调试一个多线程程序比单线程困难的多。)
线程和进程有什么区别?
调度 :进程是操作系统分配资源的一个基本单位。线程是 CPU调度的基本单位。
并发性:引入线程之后,不仅进程之间是可以并发执行的,而且在一个进程之中的多个线程也是 可以并发执行的,甚至是允许一个进程中 的全部进程并发执行。同样,不同的进程中的线程也是可以并发执行的。使得OS有 更好的并发性,提高了资源的利用率和系统吞吐量。
拥有资源:进程可以拥有资源,并且是系统拥有资源的基本单位 。线程本身并不拥有系统资源,仅有一些 能保证独立运行 的资源,这块资源的各个线程私有的。例如,线程ID、一组寄存器、栈、errno、信号屏蔽字(一个进程中pending信号只有一个,但是任意一个线程都可以处理这个信号)、调度优先级。
独立性:在同一进程中线程的独立性要比在不同的进程中独立性要低很多 。
系统开销:线程切换的开销低于进程切换的开销,
支持多处理机系统:对于传统的进程,也就是单线程进程 ,不管有多少个处理机,进程只能运行在同一个 处理机上面,但对于多线程进程,就可以将一个进程中的多个线程分配到多个处理机上面,使其并发执行,加速了进程的完成。
进程和线程的应用场景?
- 需要频繁创建销毁优先使用线程。
- 需要大量计算的优先使用线程。
- 相关性较强的使用线程,相关性较弱使用进程。
- 可能要扩展到多机分布使用进程,多核分布使用线程
什么是线程同步、异步?
同步(synchronous)就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。
异步就是和同步相对,不阻塞,同时运行。
线程之间如何同步
只要在几个线程之间共享非 final 变量,就必须使用synchronized(或 volatile)以确保一个线程可以看见另一个线程做的更改。
-
Java同步机制有4种实现方式:
-
① ThreadLocal ② synchronized( ) ③ wait() 与 notify() ④ volatile
-
目的:都是为了解决多线程中的对同一变量的访问冲突
-
ThreadLocal
-
ThreadLocal 保证不同线程拥有不同实例,相同线程一定拥有相同的实例,即为每一个使用该
-
变量的线程提供一个该变量值的副本,每一个线程都可以独立改变自己的副本,而不是与其它线程
-
的副本冲突。
-
优势:提供了线程安全的共享对象
-
与其它同步机制的区别:同步机制是为了同步多个线程对相同资源的并发访问,是为了多个线程之
-
间进行通信;而 ThreadLocal 是隔离多个线程的数据共享,从根本上就不在多个线程之间共享资源
-
,这样当然不需要多个线程进行同步了。
-
volatile
-
volatile 修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。
-
而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。
-
优势:这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。
-
缘由:Java 语言规范中指出,为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而
-
且只当线程进入或者离开同步代码块时才与共享成员变量的原始值对比。这样当多个线程同时与某
-
个对象交互时,就必须要注意到要让线程及时的得到共享成员变量的变化。而 volatile 关键字就
-
是提示 VM :对于这个成员变量不能保存它的私有拷贝,而应直接与共享成员变量交互。
-
使用技巧:在两个或者更多的线程访问的成员变量上使用 volatile 。当要访问的变量已在
-
synchronized 代码块中,或者为常量时,不必使用。
-
线程为了提高效率,将某成员变量(如A)拷贝了一份(如B),线程中对A的访问其实访问的
-
是B。只在某些动作时才进行A和B的同步,因此存在A和B不一致的情况。volatile就是用来避免这种
-
情况的。 volatile告诉jvm,它所修饰的变量不保留拷贝,直接访问主内存中的(读操作多时使用
-
较好;线程间需要通信,本条做不到)
-
Volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性。这就是说线程能够自
-
动发现 volatile 变量的最新值。Volatile 变量可用于提供线程安全,但是只能应用于非常有限的
-
一组用例:多个变量之间或者某个变量的当前值与修改后值之间没有约束。
-
您只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理
-
想的线程安全,必须同时满足下面两个条件:
-
对变量的写操作不依赖于当前值;该变量没有包含在具有其他变量的不变式中。
-
-
sleep() vs wait()
-
sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,把执行机会给其他线程,但是监
-
控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。
-
wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁
-
定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁
-
进入运行状态。
-
(如果变量被声明为volatile,在每次访问时都会和主存一致;如果变量在同步方法或者同步块中
-
被访问,当在方法或者块的入口处获得锁以及方法或者块退出时释放锁时变量被同步。)
什么是线程不安全?如何解决?(重点)
什么叫线程安全:
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。
线程安全问题都是由全局变量及静态变量引起的。
若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。
为什么ArrayList线程不安全?不安全为什么要使用?如何解决线程不安全?
一个 ArrayList ,在添加一个元素的时候,它可能会有两步来完成:
1. 在 Items[Size] 的位置存放此元素;
2. 增大 Size 的值。
在单线程运行的情况下,如果 Size = 0,添加一个元素后,此元素在位置 0,而且 Size=1;
而如果是在多线程情况下,比如有两个线程,线程 A 先将元素存放在位置 0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时 Size 仍然等于 0 (注意哦,我们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加 Size 的值。
那好,现在我们来看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而 Size 却等于 2。这就是“线程不安全”了。
https://blog.csdn.net/qq_28081081/article/details/80413669
如何解决
加锁(对象锁、锁代码块)、自旋+CAS方式(乐观锁)、使用java提供的线程安全的数据类
如何创建一个线程?有几种方法?
1.继承Thread类
2.实现Runnable接口
3.实现Callable接口
https://blog.csdn.net/zz0129/article/details/80894016
https://blog.csdn.net/itcats_cn/article/details/81149232
是使用Runnalbe接口好?还是继承Thread类好?
实现Runnable接口更好一些。
1,因为实现Runnable接口可以避免Java单继承的局限性。
当一个类继承了Thread,就不可以在继承其他类了。
而当一个类实现了Runnable,它一样可以继承其他类。
比如 class Demo extends SuperDemo implements Runnable{}
2,更符合面向对象的设计。
run()方法的作用是用来封装线程要运行的代码。
那么run()方法所属的对象,就是线程任务对象。
Thread类的子类对象即使线程对象,又是线程任务对象。耦合性很强。
有了Runnable接口,可以将线程任务和线程进行解耦,
提高了程序的扩展性。
sleep()和 wait()有什么区别?
sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,把执行机会给其他线程,但是监 控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。
wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁 定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁 进入运行状态。 (如果变量被声明为volatile,在每次访问时都会和主存一致;如果变量在同步方法或者同步块中 被访问,当在方法或者块的入口处获得锁以及方法或者块退出时释放锁时变量被同步。)
countDownLatch和cyclicBarrier、semaphore
CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;(比如主线程等某个数量的子线程:火箭发射前,需要检查各个部件,检查部件的子线程都完成后,告诉主线程去发射;或:阿姨要打扫男厕所,发现有10个人在拉屎,阿姨非常尊重男性隐私,等10个人全拉完出来了,才开始打扫)
CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;(比如某个数量的子线程约定等到一起执行:社区举行拉屎比赛,每满10个人到场就可以开拉(社区厕所真大)。或,吃流水餐,只要等满一桌十人,则大家就开始吃;或,性能测试时,等待一定数量的多个子线程都完成数据准备阶段,再同时发起请求,以达到高并发的目的)
另外,CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。
samaphore其实和锁有点类似,它一般用于控制对某组资源的访问权限。(15人想拉屎,只有5个蹲位,先找到蹲位,先拉,否则等待,拉完让出蹲位)
https://lazytest.blog.csdn.net/article/details/90379676
https://www.cnblogs.com/dolphin0520/p/3920397.html
集合相关
什么是数组?什么是链表?
数组就像身上编了号站成一排的人,要找第10个人很容易,根据人身上的编号很快就能找到。但插入、删除慢,要望某个位置插入或删除一个人时,后面的人身上的编号都要变。当然,加入或删除的人始终末尾的也快。
链表是什么
链表是一种上一个元素的引用指向下一个元素的存储结构,链表通过指针来连接元素与元素;
链表就像手牵着手站成一圈的人,要找第10个人不容易,必须从第一个人一个个数过去。但插入、删除快。插入时只要解开两个人的手,并重新牵上新加进来的人的手就可以。删除一样的道理。
Java中,ArrayList、LinkedList就是分别用数组和链表做内部实现的。
数组和链表的区别
链表通过指针来连接元素与元素,数组则是把所有元素按次序依次存储。
链表的插入删除元素相对数组较为简单,不需要移动元素,且较为容易实现长度扩充,但是寻找某个元素较为困难;
数组寻找某个元素较为简单,但插入与删除比较复杂,由于最大长度需要再编程一开始时指定,故当达到最大长度时,扩充长度不如链表方便。
相同:两种结构均可实现数据的顺序存储,构造出来的模型呈线性结构。
java集合和数组的优缺点
区别
数组特点:大小固定,只能存储相同数据类型的数据
集合特点:大小可动态扩展,可以存储各种类型的数据
转换
数组转换为集合:
Arrays.asList(数组)
示例:
1 2 3 4 5 |
|
集合转换为数组:
集合.toArray();
示例:
1 2 3 4 5 6 |
|
什么是哈希表
哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
数组的特点是:寻址容易,插入和删除困难;
而链表的特点是:寻址困难,插入和删除容易。
那么我们能不能综合两者的特性,做出一种寻址容易,插入删除也容易的数据结构?答案是肯定的,这就是我们要提起的哈希表,哈希表有多种不同的实现方法,我接下来解释的是最常用的一种方法——拉链法,我们可以理解为“链表的数组”
ArrayList底层实现方式
ArrayList的底层数据结构就是一个数组,数组元素的类型为Object类型,对ArrayList的所有操作底层都是基于数组的。
对ArrayList进行添加元素的操作的时候是分两个步骤进行的,即第一步先在object[size]的位置上存放需要添加的元素;第二步将size的值增加1。由于这个过程在多线程的环境下是不能保证具有原子性的,因此ArrayList在多线程的环境下是线程不安全的。
https://blog.csdn.net/weixin_36378917/article/details/81812210
LinkedList底层实现方式
LinkedList是通过双向链表去实现的,既然是链表实现那么它的随机访问效率比ArrayList要低,顺序访问的效率要比较的高。每个节点都有一个前驱(之前前面节点的指针)和一个后继(指向后面节点的指针),效果如下图:
https://www.jianshu.com/p/ea5b7dd7dc01
1,使用for适合循环ArrayLIst以及数组,当大批量的循环LinkedList时程序将会卡死,for适合循环数组结构,通过下标去遍历。
2,使用foreach适合循环LinkedList,使用双链表结构实现的应当使用foreach循环。
HashMap底层实现方式
HashMap采用Entry数组来存储key-value对,每一个键值对组成了一个Entry实体,Entry类实际上是一个单向的链表结构,它具有Next指针,可以连接下一个Entry实体,以此来解决Hash冲突的问题。
从上图我们可以发现数据结构由数组+链表组成,一个长度为16的数组中,每个元素存储的是一个链表的头结点。那么这些元素是按照什么样的规则存储到数组中呢。一般情况是通过hash(key.hashCode())%len获得,也就是元素的key的哈希值对数组长度取模得到。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存储在数组下标为12的位置。
HashMap里面实现一个静态内部类Entry,其重要的属性有 hash,key,value,next。
HashMap里面用到链式数据结构的一个概念。上面我们提到过Entry类里面有一个next属性,作用是指向下一个Entry。打个比方, 第一个键值对A进来,通过计算其key的hash得到的index=0,记做:Entry[0] = A。一会后又进来一个键值对B,通过计算其index也等于0,现在怎么办?HashMap会这样做:B.next = A,Entry[0] = B,如果又进来C,index也等于0,那么C.next = B,Entry[0] = C;这样我们发现index=0的地方其实存取了A,B,C三个键值对,他们通过next这个属性链接在一起。所以疑问不用担心。也就是说数组中存储的是最后插入的元素。到这里为止,HashMap的大致实现,我们应该已经清楚了。
JDK 1.8的 改变
,数据结构的存储由数组+链表的方式,变化为数组+链表+红黑树的存储方式,当链表长度超过阈值(8)时,将链表转换为红黑树。在性能上进一步得到提升。
https://www.cnblogs.com/java-jun-world2099/p/9258605.html
ArrayList 和 Vector 的区别
(1)同步性:Vector是线程安全的,用synchronized实现线程安全,而ArrayList是线程不安全的,如果只有一个线程会访问到集合,那最好使用ArrayList,因为它不考虑线程安全,效率会高些;如果有多个线程会访问到集合,那最好是使用Vector,因为不需要我们再去考虑和编写线程安全的代码。
(2)数据容量增长:二者都有一个初始容量大小,采用线性连续存储空间,当存储的元素的个数超过了容量时,就需要增加二者的存储空间,Vector增长原来的一倍,ArrayList增加原来的0.5倍。
https://blog.csdn.net/weixin_39684625/article/details/80652809
ArrayList 为什么线程不安全
对ArrayList进行添加元素的操作的时候是分两个步骤进行的,即第一步先在object[size]的位置上存放需要添加的元素;第二步将size的值增加1。由于这个过程在多线程的环境下是不能保证具有原子性的,因此ArrayList在多线程的环境下是线程不安全的
HashMap 和 Hashtable 的区别
主要是线程安全与不安全的区别,详细的见:
https://blog.csdn.net/wangxing233/article/details/79452946
HashMap 为什么线程不安全:resize()死循环:http://www.importnew.com/22011.html
HashMap与LinkedHashMap,和TreeMap的区别
l (1)HashMap是一个最常用的Map,它根据键的hashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度。HashMap最多只允许一条记录的键为null,不允许多条记录的值为null。HashMap不支持线程的同步,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要同步,可以用Collections.synchronizedMap(HashMap map)方法使HashMap具有同步的能力。
l (2)Hashtable与HashMap类似,不同的是:它不允许记录的键或者值为空;它支持线程的同步,即任一时刻只有一个线程能写Hashtable,然而,这也导致了Hashtable在写入时会比较慢。
l (3)LinkedHashMap保存了记录的插入顺序,在用Iteraor遍历LinkedHashMap时,先得到的记录肯定是先插入的。在遍历的时候会比HashMap慢。有HashMap的全部特性。
l (4)TreeMap能够把它保存的记录根据键排序,默认是按升序排序,也可以指定排序的比较器。当用Iteraor遍历TreeMap时,得到的记录是排过序的。TreeMap的键和值都不能为空。
https://www.cnblogs.com/baizhanshi/p/5810495.html
上面这个链接里的代码有点小问题。
List、Map、Set 三个接口,存取元素时,各有什么特点
Set里面不允许有重复的元素,
存元素:add方法有一个boolean的返回值,当集合中没有某个元素,此时add方法可成功加入该元素时,则返回true;当集合含有与某个元素equals相等的元素时,此时add方法无法加入该元素,返回结果为false。
取元素:没法说取第几个,只能以Iterator接口取得所有的元素,再逐一遍历各个元素。
List表示有先后顺序的集合,
存元素:多次调用add(Object)方法时,每次加入的对象按先来后到的顺序排序,也可以插队,即调用add(int index,Object)方法,就可以指定当前对象在集合中的存放位置。
取元素:
方法1:Iterator接口取得所有,逐一遍历各个元素
方法2:调用get(index i)来明确说明取第几个。使用此接口能够精确的控制每个元素插入的位置。用户能够使用索引(元素在List中的位置,类似于数组下标)来访问List中的元素,这类似于Java的数组。
Map是双列的集合,存放用put方法:put(obj key,obj value),每次存储时,要存储一对key/value,不能存储重复的key,这个重复的规则也是按equals比较相等。
取元素:用get(Object key)方法根据key获得相应的value。
也可以获得所有的key的集合,还可以获得所有的value的集合,
还可以获得key和value组合成的Map.Entry对象的集合。
List以特定次序来持有元素,可有重复元素。Set 无法拥有重复元素,内部排序。Map 保存key-value值,value可多值。
去掉一个 Vector 集合中重复的元素
通过Vector.contains()方法判断是否包含该元素,如果没有包含就添加到新的集合当中,适用于数据较小的情况下
还有一种简单的方式,遍历Vector,放入set、SortdSet、HashSet等
https://blog.csdn.net/cw2004100021124/article/details/39499529
Collection 和 Collections 的区别
1、java.util.Collection 是一个集合框架的父接口。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式。
Collection
├List
│├LinkedList
│├ArrayList
│└Vector
│ └Stack
└Set
其里面包含的方法有:
2、java.util.Collections 是一个包装类。它包含有各种有关集合操作的静态多态方法。此类不能实例化,就像一个工具类,服务于Java的Collection框架。他提供一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作。
Set 里的元素是不能重复的,那么用什么方法来区分重复与否呢?是用==还是equals()?它们有何区别?
==:
基本类型:比较的是值是否相同
引用类型:比较的是地址值是否相同
equals():
引用类型:默认情况下,比较的是地址值,可进行重写,比较的是对象的成员变量值是否相同
如果一个类没有自己定义 equals 方法,它默认的 equals 方法(从 Object 类继承
的)就是使用==操作符,也是在比较两个变量指向的对象是否是同一对象,这时候使用
equals 和使用==会得到同样的结果,如果比较的是两个独立的对象则总返回 false。如果你
编写的类希望能够比较该类创建的两个实例对象的内容是否相同,那么你必须覆盖 equals
方法,由你自己写代码来决定在什么情况即可认为两个对象的内容是相同的
Iterator的作用
用于遍历集合中的元素,比for 和for each的好处:
不会因为节点的删除报异常,自己写的数据结构也可以实现它。
HashSet和TreeSet有什么区别,什么时候用它们
基本的
1、TreeSet 是二差树实现的,Treeset中的数据是自动排好序的,不允许放入null值。
2、HashSet 是哈希表实现的,HashSet中的数据是无序的,可以放入null,但只能放入一个null,两者中的值都不能重复,就如数据库中唯一约束。
3、HashSet要求放入的对象必须实现HashCode()方法,放入的对象,是以hashcode码作为标识的,而具有相同内容的 String对象,hashcode是一样,所以放入的内容不能重复。但是同一个类的对象可以放入不同的实例 。
时间复杂度
HashSet是由一个hash表来实现的, 因此,它的元素是无序的。 add(),remove(),contains()方法的时间复杂度是O(1)。
TreeSet是由一个树形的结构来实现的,它里面的元素是有序的。因此,add(),remove(),contains()方法的时间复杂度是O(logn)。
利用TreeSet保存自定义类对象的时候,自定义所在的类一定要实现Comparable接口,如果没有实现这个接口那么就无法区分大小关系,而且在TreeSet中如果要进行排序,那么就要将所有的字段都进行比较,就是说在TreeSet中是依靠comparato()方法返回的是不是0来判断是不是重复元素的。
TreeSet 依靠的是Comparable 来区分重复数据;
HashSet 依靠的是hashCode()、equals()来区分重复数据
比较Java中几个常用集合添加元素的效率
这里只插入了int型作为实验,不一定正确
预知总大小,并设置总大小为集合大小,使之不扩容的情况下:
ArrayList添加元素程序运行时间为:5ms
LinkedList添加10万个元素程序运行时间为:7ms
Set添加10万个元素程序运行时间为:18ms
TreeSet添加10万个元素程序运行时间为:34ms
HashMap添加10万个元素程序运行时间为:8ms
TreeMap添加10万个元素程序运行时间为:19ms
如果ArrayList不设置初始大小,速度慢于LinkedList
https://www.cnblogs.com/Dylansuns/p/6746564.html
数据结构与算法
动态规划