首页 > 代码库 > Java 经典问题
Java 经典问题
九种基本类型及封装类
基本类型 | boolean | byte | char | short | int | long | double | void |
---|---|---|---|---|---|---|---|---|
二进制位数 | 1 | 8(一字节) | 16(2字节) | 16(2字节) | 32(4字节) | 64(8字节) | 64(8字节) | -- |
封装器类 | Boolean | Byte | Character | Short | Integer | Long | Double | Void |
switch
语句后的控制表达式只能是short
、char
、int
、long
整数类型和枚举类型,不能是float,double和boolean类型。String
类型是java7
开始支持。
位运算符
左移
(<<)右移
(>>):int是32位,最高位是符号位,0代表正数,1代表负数,负数以补码的形式存储在计算机中。右移规则:最高位是什么(0或者1),右移的时候左边就补什么。即正数右移用0补位左边
,负数右移用1补位左边
。无符号右移
(>>>):不管是负数还是正数,右移总是左边补0
。与
运算(&)或
运算(|)非
运算(~)异或
运算(^):位相同为0
,相异为1
-5右移3位后结果为-1,-1的二进制为:
1111 1111 1111 1111 1111 1111 1111 1111 // (用1进行补位)
-5无符号右移3位后的结果 536870911 换算成二进制:
0001 1111 1111 1111 1111 1111 1111 1111 // (用0进行补位)
应用:不用临时变量交换两个数
void swap(int argc, char *argv[])
{
a = a ^ b;
b = b ^ a;
a = a ^ b;
}
for循环,ForEach,迭代器效率
直接for循环效率最高,其次是迭代器和 ForEach操作。
其实ForEach 编译成字节码
之后,使用的是迭代器实现
的。
synchronized和volatile
volatile
仅能使用在变量
级别;synchronized
则可以使用在变量
、方法
、和类
级别的。volatile
保证了变量的可见性
,synchronized
保证了原子性
和可见性
。
volatile
原理:首先我们要先意识到有这样的现象,编译器为了加快程序运行的速度,对一些变量的写操作会先在寄存器或者是CPU缓存上进行
,最后才写入内存。而在这个过程,变量的新值对其他线程是不可见的,而volatile
的作用就是使它修饰的变量的读写操作都必须在内存中进行
。volatile
告诉JVM
, 它所修饰的变量不保留拷贝,直接访问主内存中的
。
volatile与synchronized
volatile
本质是在告诉JVM
当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住
。volatile
仅能使用在变量级别,synchronized
则可以使用在变量
,方法
.volatile
仅能实现变量的修改可见性
,但不具备原子特性
,而synchronized
则可以保证变量的修改可见性
和原子性
。volatile不会造成线程的阻塞
,而synchronized
可能会造成线程的阻塞。volatile
标记的变量不会被编译器优化
,而synchronized
标记的变量可以被编译器优化。
volatile不能保证原子性原因:线程A修改了变量还没结束时,另外的线程B可以看到已修改的值,而且可以修改这个变量,而不用等待A释放锁,因为Volatile 变量没上锁。
注意
声明为volatile
的简单变量如果当前值由该变量以前的值相关
,那么volatile关键字不起作用
。
也就是说如下的表达式都不是原子操作:
n = n + 1 ;
n ++ ;
只有当变量的值和自身上一个值无关时对该变量的操作才是原子级别的,如n = m + 1。
Java内存模型的抽象(volatile)
在java
中,所有实例域、静态域和数组元素存储在堆内存中,堆内存
在线程之间共享(本文使用“共享变量
”这个术语代指实例域,静态域和数组元素)。局部变量
,方法定义参数
和异常处理器参数不会在线程之间共享
,在栈内存中,不需要同步处理,因为栈内存是线程独享的,它们不会有内存可见性问题,也不受内存模型的影响。
Java线程之间的通信由Java内存模型(本文简称为JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存
(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本(寄存器或CPU缓存)本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。Java内存模型的抽象示意图如下:
从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:
- 首先,线程A把本地内存A中
更新过的共享变量刷新到主内存中去
。 - 然后,线程B到
主内存中去读取线程A之前已更新过的共享变量
。
下面通过示意图来说明这两个步骤:
如上图所示,本地内存A和B有主内存中共享变量x的副本。假设初始时,这三个内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了。
从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。
equals与==的区别
==
常用于比较原生类型
,而equals()
方法用于检查对象的相等性
。另一个不同的点是:如果==
和equals()
用于比较对象,当两个引用地址相同,== 返回true
。而equals()
可以返回true
或者false
主要取决于重写实现。最常见的一个例子,字符串的比较,不同情况 ==
和equals()
返回不同的结果。
看Object源码:
public boolean equals(Object obj) {
return (this == obj);
}
==
表示的是比较两个对象实例的内存地址是否相同
。如果不重写equal()
,就和==
等效,
- 相等(相同)的对象必须具有相等的哈希码(或者散列码)。
- 如果两个对象的
hashCode
相同,它们并不一定相同。
术语来讲的区别:
==
是判断两个变量或实例是不是指向同一个内存空间
。equals
是判断两个变量或实例所指向的内存空间的值
是不是相同。==
指引用
是否相同equals()
指的是值
是否相同
hashCode作用
以
java.lang.Object
来理解JVM
每new
一个Object
,它都会将这个Object
丢到一个Hash哈希表
中去,这样的话,下次做Object
的比较或者取这个对象的时候,它会根据对象的hashcode
再从Hash表
中取这个对象。这样做的目的是提高取对象的效率
。
具体过程是这样:
new Object()
,JVM
根据这个对象的Hashcode
值放入到对应的Hash表
对应的Key
上,如果不同的对象却产生了相同的hash值
,也就是发生了Hash key
相同导致冲突
的情况,那么就在这个Hash key
的地方产生一个链表,将所有产生相同hashcode
的对象放到这个单链表上串在一起。- 比较两个对象的时候,首先根据他们的
hashcode
去hash表
中找他的对象,当两个对象的hashcode
相同,那么就是说他们这两个对象放在Hash
表中的同一个key
上,那么他们一定在这个key
上的链表
上。那么此时就只能根据Object
的equal
方法来比较这个对象是否equal
。当两个对象的hashcode
不同的话,肯定他们不能equal。
java.lang.Object
中对hashCode
的约定:
- 在一个应用程序执行期间,如果一个对象的
equals
方法做比较所用到的信息没有被修改的话,则对该对象调用hashCode
方法多次,它必须始终如一地返回同一个整数。 - 如果两个对象根据
equals(Object o)
方法是相等的,则调用这两个对象中任一对象的hashCode
方法必须产生相同的整数结果。 - 如果两个对象根据
equals(Object o)
方法是不相等的,则调用这两个对象中任一个对象的hashCode
方法,不要求产生不同的整数结果
。但如果能不同,则可能提高散列表的性能。
Object的公用方法
clone
保护方法,只有实现了Cloneable
接口才可以调用,否则抛异常getClass
final
方法,获得运行时类型
toString
equals
hashCode
wait
就是使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。wait方法一直等待,直到获得锁或者被中断。wait设定一个超时间隔,如果在规定时间内没有获得锁就返回。
调用该方法后当前线程进入睡眠状态,直到以下事件发生。- 其他线程调用了该对象的
notify
方法。 - 其他线程调用了该对象的
notifyAll
方法。 - 其他线程调用了
interrupt
中断该线程。 - 时间间隔到了。
此时该线程就可以被调度了,如果是被中断的话就抛出一个InterruptedException异常。
- 其他线程调用了该对象的
notify
notifyAll
Java四种引用 --- 这里指的是“引用“,不是对象
强引用
平常我们使用对象的方式
Object object = new Object();
如果一个对象具有强引用
,它就不会被垃圾回收器回收
。即使当前内存空间不足,JVM
也不会回收它,而是抛出OutOfMemoryError
错误,使程序异常终止。例如下面的代码:
public class Main {
public static void main(String[] args) {
new Main().fun1();
}
public void fun1() {
Object object = new Object();
Object[] objArr = new Object[1000];
}
}
当运行至Object[] objArr = new Object[1000];
这句时,如果内存不足,JVM
会抛出OOM
错误也不会回收object
指向的对象。不过要注意的是,当fun1
运行完之后,object
和objArr
都已经不存在了,所以它们指向的对象都会被JVM回收。
但如果想中断强引用和某个对象之间的关联,可以显式地将引用赋值为null
,这样一来的话,JVM
在合适的时间就会回收该对象。
软引用
软引用通过
SoftReference
创建,在使用软引用
时,如果内存的空间足够,软引用就能继续被使用,而不会被垃圾回收器回收,只有在内存不足时,软引用才会被垃圾回收器回收
。
软引用的这种特性使得它很适合用来解决 OOM
问题,实现缓存机制
,例如:图片缓存、网页缓存等等……
软引用可以和一个引用队列(ReferenceQueue
)联合使用,如果软引用所引用的对象被JVM
回收,这个软引用就会被加入到与之关联的引用队列中。
弱引用
事实上软引用和弱引用非常类似,两者的区别在于:只具有弱引用的对象拥有的生命周期更短暂。因为当 JVM 进行垃圾回收,一旦发现弱引用对象,
无论当前内存空间是否充足,都会将弱引用回收
。不过由于垃圾回收器是一个优先级较低的线程,所以并不一定能迅速发现弱引用对象。
弱引用可以和一个引用队列(ReferenceQueue
)联合使用,如果弱引用所引用的对象被JVM
回收,这个弱引用就会被加入到与之关联的引用队列中。
虚引用
虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会影响对象的生命周期。如果一个对象仅持有虚引用,那么它相当于没有引用,
在任何时候都可能被垃圾回收器回收
。
- 强引用:不管什么时候都不会被回收。
- 软引用:当内存不足的时候,JVM垃圾回收会回收。
- 弱引用:不管内存足不足,只要发生JVM垃圾回收就会回收。
- 虚引用:随时都可能会被回收。
小结
引用和引用队列提供了一种通知机制,允许我们知道对象已经被销毁或者即将被销毁。
GC
要回收一个对象的时候,如果发现该对象有软、弱、虚引用的时候,会将这些引用加入到注册的引用队列
中。软引用和弱引用差别不大,JVM都是先把SoftReference
和WeakReference
中的referent
字段值设置成null
,之后加入到引用队列
;而虚引用则不同,如果某个堆中的对象,只有虚引用,那么JVM
会将PhantomReference
加入到引用队列中,JVM
不会自动将referent
字段值设置成null
。
实际应用:利用软引用和弱引用缓存解决OOM问题。
如:Bitmap的缓存
设计思路是:用一个
HashMap
来保存图片的路径
和相应图片对象(Bitmap
)的软引用
之间的映射关系,在内存不足时,JVM
会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM
的问题。在Android开发中对于大量图片下载会经常用到。
wait()、notify()和sleep()
wait()和notify()
wait()
和notify()
是直接隶属于Object
类,在JAVA
中的Object
类型中,都是带有一个内存锁的,在有线程获取该内存锁后,其它线程无法访问该内存,从而实现JAVA
中简单的同步、互斥操作。明白这个原理,就能理解为什么synchronized(this)
与synchronized(static XXX)
的区别了,synchronized
就是针对内存区块申请内存锁,this
关键字代表类的一个对象,所以其内存锁是针对相同对象的互斥操作
,而static
成员属于类专有,其内存空间为该类所有成员共有,这就导致synchronized()
对static
成员加锁,相当于对类加锁
,也就是在该类的所有成员间实现互斥,在同一时间只有一个线程可访问该类的实例
。wait只能由持有对像锁的线程来调用
。
Obj.wait()
与Obj.notify()
必须要与synchronized(Obj)
一起使用,从功能上来说wait
就是说线程在获取对象锁后,主动释放对象锁
,同时本线程休眠
。直到有其它线程调用对象的notify()
唤醒该线程,才能继续获取对象锁,并继续执行。相应的notify()
就是对对象锁的唤醒操作。但有一点需要注意的是notify()调用后,并不是马上就释放对象锁的,而是在相应的synchronized(){}语句块执行结束
,自动释放锁后,JVM
会在wait()
对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执行。这样就提供了在线程间同步、唤醒的操作。Thread.sleep()
与Object.wait()
二者都可以暂停当前线程,释放CPU控制权,主要的区别在于Object.wait()在释放CPU同时,释放了对象锁的控制
。
wait()
:促使当前线程等待直到另外一个线程调用这个对象的notify()
方法唤醒。和synchronized
块使用的时候,synchronized
获取对象的锁以后,可以通过wait()
方法释放,同时阻塞当前线程,停止执行(这也是和sleep
的区别)。
public static void firstMethod(){
synchronized (a){
System.out.println(Thread.currentThread().getName() + " firstMethod--死锁");
try {
// Thread.sleep(10);
a.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b){
System.out.println(Thread.currentThread().getName() + " firstMethod--解锁");
}
}
}
public static void seconedMethod(){
synchronized (b){
System.out.println(Thread.currentThread().getName() + " seconedMethod--死锁");
synchronized (a){
System.out.println(Thread.currentThread().getName() + " seconedMethod--解锁");
a.notify();
}
}
}
如果用两个线程分别执行这两个方法
public static void main(String[] args) {
Runnable runnable1 = new Runnable() {
@Override
public void run() {
firstMethod();
}
};
Runnable runnable2 = new Runnable() {
@Override
public void run() {
seconedMethod();
}
};
Thread thread1 = new Thread(runnable1);
Thread thread2 = new Thread(runnable2);
thread1.start();
thread2.start();
}
如果是用sleep
方法替换掉wait
方法,就是一个死锁
,线程thread1
先执行拿到a
对象的锁,然后阻塞10ms
(并没有释放锁),thread2
然后拿到对象b
的锁,这时候seconedMethod
需要a
对象的锁,但是firstMethod
并没有释放,然后10ms
过后,firstMethod
需要b
的锁,然后b
的锁也没有在seconedMethod
方法中释放,两个线程相互等待对方释放锁,就形成了死锁。
运行结果:
Thread-0 firstMethod--死锁
Thread-1 seconedMethod--死锁
如果不使用sleep
而是使用wait
方法,就不会发生死锁。因为wait
释放了firstMethod
中的a
对象的锁,当seconedMethod
需要a
对象锁的时候就可以用了。
运行结果:
Thread-0 firstMethod--死锁
Thread-1 seconedMethod--死锁
Thread-1 seconedMethod--解锁
Thread-0 firstMethod--解锁
notify()
:唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程(随机)
。直到当前的线程放弃此对象上的锁,才能继续执行被唤醒的线程
。
sleep()
通过
Thread.sleep()
使当前线程暂停执行一段时间,让其他线程有机会继续执行,但它并不释放对象锁
。也就是说如果有synchronized
同步块,其他线程仍然不能访问共享数据。注意该方法要捕捉异常。
public class ThreadLock {
Object lock = new Object();
int num = 0;
public static void main(String[] args) {
ThreadLock test = new ThreadLock();
Runnable runnable = new Runnable() {
@Override
public void run() {
test.method2();
}
};
Thread thread1 = new Thread(runnable);
thread1.start();
test.method1();
}
public void method1(){
synchronized (lock){
try {
Thread.sleep(1000);
// lock.wait(1000);
num += 100;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void method2(){
synchronized (lock){
num += 9;
System.out.println(num);
}
}
}
因为在main线程调用方法,因此先执行主线程的method1,对象锁被主线程拿走了,那么子线程执行method2的时候就需要等待1秒后把锁还回来。
1秒后输出结果:
109
如果替换成lock.wait(1000);
lock.wait(1000)
会让当前线程(main线程
)睡眠1秒,同时释放synchronized
的对象锁,因此小于1秒输出9
synchronized和lock
几个概念
- 共享变量(shared variable):多个线程都能访问的变量。
- 变量可见性(variable visibility):
一个线程更新了共享变量,对其他线程立刻可见
。 - 互斥(mutual exclusion ):
几个线程中的任何一个不能与其他一个或多个同时操作一个变量
。 - 临界区(critical section):访问共享资源的一段代码块。
synchronized
- 保证共享变量的可见性:
变量缓存与编译器指令优化会导致变量修改的不可见性
。 - 保证共享变量的互斥性:
同一时刻只能有一个线程对共享变量的修改(注意修改一次,是先读再写,是两个操作)
。
特点:
- 当程序运行到非静态的
synchronized
同步方法上时,自动获得与正在执行代码类的当前实例(this实例
)有关的锁。 - 如果两个线程要执行一个类中的
synchronized
方法,并且两个线程使用相同的实例来调用方法,那么一次只能有一个线程能够执行方法,另一个需要等待,直到锁被释放。也就是说:如果一个线程在对象上获得一个锁,就没有任何其他线程可以进入(该对象的)类中的任何一个同步方法。 线程可以获得多个锁
。比如,在一个对象的同步方法里面调用另外一个对象的同步方法,则获取了两个对象的同步锁。- 线程同步方法是通过锁来实现,每个对象都有且仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的其他同步方法。
- 对于同步,要时刻清醒在哪个对象上同步,这是关键。
死锁是线程间相互等待锁造成的
。
lock
lock提供了如下的方法:
void lock()
,获取一个锁,如果锁当前被其他线程获得,当前的线程将被休眠
。boolean tryLock()
,尝试获取一个锁,如果当前锁被其他线程持有
,则返回false
,不会使当前线程休眠
。boolean tryLock(long timeout,TimeUnit unit)
,如果获取了锁定立即返回true,如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false。void lockInterruptibly()
,如果获取了锁定立即返回,如果没有获取锁定,当前线程处于休眠状态,直到或者锁定,或者当前线程被别的线程中断。
synchronized和lock区别
synchronized
是在JVM
层面上实现的,如果代码执行时出现异常,JVM
会自动释放monitor
锁。而lock
代码是用户写的,需要用户来保证最终释放掉锁。lock
提供了一个重要的方法newConditon()
,ConditionObject
有await()
、signal()
、signalAll()
,类似于Ojbect
类的wait()
、notify()
、notifyAll()
。这些方法都是用来实现线程间通信。lock
将synchronized
的互斥性和线程间通信分离开来,一个lock
可以有多个condition
。另外lock
的signal
可以实现公平的通知,而notify
是随机从锁等待队列中唤醒某个进程。- 性能上来说,在多线程竞争比较激烈地情况下,
lock比synchronize高效得多
。
public class ThreadLock {
public static void main(String[] args) {
Test test = new Test();
Lock lock = new ReentrantLock();
Runnable runnable = new Runnable() {
@Override
public void run() {
lock.lock();
for (int i = 0; i < 50 ; i++) {
test.setX(1);
System.out.println(Thread.currentThread().getName() + " : " +test.getX());
}
lock.unlock();
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
}
static class Test{
private int x = 100;
public int getX(){
return x;
}
public void setX(int y){
x = x - y;
}
}
}
ReentrantLock与synchronized的比较
ReentrantLocak(可重入锁)
简单来说,
它有一个与锁相关的获取计数器
,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1
,然后锁需要被释放两次才能获得真正释放。
ReentrantLock提供了synchronized类似的功能和内存语义。
不同
ReentrantLock
功能性方面更全面,比如时间锁等候,可中断锁等候,锁投票等,因此更有扩展性。在多个条件变量和高度竞争锁的地方,用ReentrantLock
更合适,ReentrantLock
还提供了Condition
,对线程的等待和唤醒等操作更加灵活,一个ReentrantLock
可以有多个Condition
实例,所以更有扩展性。ReentrantLock
的性能比synchronized
会好点。ReentrantLock
提供了可轮询的锁请求,他可以尝试的去取得锁,如果取得成功则继续处理,取得不成功,可以等下次运行的时候处理,所以不容易产生死锁,而synchronized
则一旦进入锁请求要么成功,要么一直阻塞,所以更容易产生死锁。
缺点
lock
必须在finally 块中释放
。否则,如果受保护的代码将抛出异常,锁就有可能永远得不到释放!这一点区别看起来可能没什么,但是实际上,它极为重要。忘记在 finally 块中释放锁,可能会在程序中留下一个定时炸弹,当有一天炸弹爆炸时,您要花费很大力气才能找到源头在哪。而使用同步,JVM 将确保锁会获得自动释放
。- 当
JVM
用synchronized
管理锁定请求和释放时,JVM
在生成线程转储时能够包括锁定信息。这些对调试非常有价值,因为它们能标识死锁或者其他异常行为的来源。Lock
类只是普通的类,JVM
不知道具体哪个线程拥有Lock
对象。
ArrayList,LinkedList和Vector
ArrayList
和Vector
都是基于数组
实现的,所以查询效率高,插入效率低
。LinkedList
基于双向链表实现的,所以插入效率高,查询效率低
。Vector
使用了synchronized
方法,所以线程安全,性能比ArrayList
低。LinkedList
实现了List接口
,还提供了额外的get
,remove
,insert
方法在LinkedList
的首部或尾部,这些操作使LinkedList
可被用作栈(Stack
),队列(Queue
)或双向队列(deque
)。ArrayList
和LinkedList
允许null元素,重复元素
。
HashMap和HashTable
都实现了Map接口
HashMap允许key为null,value为null而HashTable不允许
,如果新加入的key
和之前重复了,会覆盖
之前的value
。HashTable线程安全,而HashMap不是线程安全
。因此单线程下HashTable
比HashMap
慢。HashMap不能保证随着时间推移Map中的元素次序是不变的
。Hashtable中hash数组默认大小是11
,增加的方式是old*2+1
。HashMap中hash数组的默认大小是16
,而且一定是2的指数
。
ConcurrentHashMap
锁分段技术
HashTable
容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问HashTable的线程都必须竞争同一把锁
,那假如容器里有多把锁,每一把锁用于锁容器其中一部分数据
,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap
所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问
。
HashSet
实现了
Set接口
,HashSet< T >
本质就是一个HashMap<T , Object>
,把HashMap
的key
作为HashSet
的值,HashMap
的value
是一个固定的Object
对象,因为HashMap
的key
是不允许重复
的,所以HashSet
里的元素也是不能重复
的,也可以看出HashSet的查询效率很高
。
String,StringBuilder和StringBuffer
CharSequence
接口:一个字符序列
。String
,StringBuilder
和StringBuffer
都实现了它。String
类:是常量,不可变.StringBuilder
类:只可以在单线程的情况下进行修改(线程不安全
),字符串拼接用,除了StringBuffer
可用场景外。StringBuffer
类:可以在多线程的情况下进行改变(线程安全
),比如:在http
请求中拼接。StringBuilder
比StringBuffer
效率高,应该尽量使用StringBuilder
。
Excption与Error包结构
结构图:
Throwable
Throwable
是Java
语言中所有错误或异常的超类。Throwable
包含两个子类:Error
和Exception
。它们通常用于指示发生了异常情况。Throwable
包含了其线程创建时线程执行堆栈的快照,它提供了printStackTrace()
等接口用于获取堆栈跟踪数据等信息。
Exception
Exception
及其子类是Throwable
的一种形式,它指出了合理的应用程序想要捕获的条件。
RuntimeException
RuntimeException
是那些可能在Java
虚拟机正常运行期间抛出的异常的超类。- 编译器不会检查
RuntimeException
异常。 例如,除数为零时,抛出ArithmeticException
异常。RuntimeException
是ArithmeticException
的超类。当代码发生除数为零的情况时,倘若既"没有通过throws声明抛出ArithmeticException异常",也"没有通过try...catch...处理该异常",也能通过编译。这就是我们所说的"编译器不会检查RuntimeException异常"!
- 如果代码会产生
RuntimeException
异常,则需要通过修改代码进行避免
。 例如,若会发生除数为零的情况,则需要通过代码避免该情况的发生!
Error
- 和
Exception
一样,Error
也是Throwable
的子类。 它用于指示合理的应用程序不应该试图捕获的严重问题,大多数这样的错误都是异常条件。 和RuntimeException一样, 编译器也不会检查Error
。
Interface与abstract类的区别
参数 | 抽象类 | 接口 |
---|---|---|
默认的方法实现 | 它可以有默认的方法实现 | 接口完全是抽象的。它根本不存在方法的实现 |
实现 | 子类使用extends关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现。 | 子类使用关键字implements来实现接口。它需要提供接口中所有声明的方法的实现 |
构造器 | 抽象类可以有构造器 | 接口不能有构造器 |
与正常Java类的区别 | 除了你不能实例化抽象类之外,它和普通Java类没有任何区别 | 接口是完全不同的类型 |
访问修饰符 | 抽象方法可以有public、protected和default这些修饰符 | 接口方法默认修饰符是public。你不可以使用其它修饰符。 |
main方法 | 抽象方法可以有main方法并且我们可以运行它 | 接口没有main方法,因此我们不能运行它。 |
多继承 | 抽象类可以继承一个类和实现多个接口 | 接口只可以继承一个或多个其它接口 |
速度 | 它比接口速度要快 | 接口是稍微有点慢的,因为它需要时间去寻找在类中实现的方法。 |
添加新方法 | 如果你往抽象类中添加新的方法,你可以给它提供默认的实现。因此你不需要改变你现在的代码。 | 如果你往接口中添加方法,那么你必须改变实现该接口的类。 |
静态内部类和非静态内部类
相同点
- 内部类都可以用
public
,protected
,private
修饰。- 方法中都可以调用
外部类的静态成员变量
。
不同点
- 静态内部类可以声明静态和非静态成员变量,
非静态内部类只能声明非静态成员变量
。- 静态内部类可以声明静态和非静态方法,
非静态内部类只能声明非静态方法
。静态内部类不能调用外部类的非静态成员变量
(静态方法和非静态方法都一样),非静态内部类都可以调用。
泛型擦除
一篇好博客
泛型在JDK5
以后才有的,擦除是为了兼容之前没有的使用泛型的类库和代码
。如:ArrayList< String >
和ArrayList< Integer >
在编译器编译的时候都变成了ArrayList
。
List<Integer> list = new ArrayList<Integer>();
Map<Integer, String> map = new HashMap<Integer, String>();
System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
System.out.println(Arrays.toString(map.getClass().getTypeParameters()));
/* Output
[E]
[K, V]
*/
我们期待的是得到泛型参数的类型,但是实际上我们只得到了一堆占位符
。
public class Main<T> {
public T[] makeArray() {
// error: Type parameter ‘T‘ cannot be instantiated directly
return new T[5];
}
}
我们无法在泛型内部创建一个T
类型的数组,原因也和之前一样,T
仅仅是个占位符,并没有真实的类型信息,实际上,除了new
表达式之外,instanceof
操作和转型(会收到警告)在泛型内部都是无法使用的,而造成这个的原因就是之前讲过的编译器对类型信息进行了擦除。
public class Main<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
public static void main(String[] args) {
Main<String> m = new Main<String>();
m.set("findingsea");
String s = m.get();
System.out.println(s);
}
}
/* Output
findingsea
*/
虽然有类型擦除的存在,使得编译器在泛型内部其实完全无法知道有关T
的任何信息,但是编译器可以保证重要的一点:内部一致性,也是我们放进去的是什么类型的对象,取出来还是相同类型的对象
,这一点让Java的泛型起码还是有用武之地的。
代码片段展现就是编译器确保了我们放在T
上的类型的确是T
(即便它并不知道有关T
的任何类型信息)。这种确保其实做了两步工作:
set()
处的类型检验get()
处的类型转换
这两步工作也成为边界动作。
public class Main<T> {
public List<T> fillList(T t, int size) {
List<T> list = new ArrayList<T>();
for (int i = 0; i < size; i++) {
list.add(t);
}
return list;
}
public static void main(String[] args) {
Main<String> m = new Main<String>();
List<String> list = m.fillList("findingsea", 5);
System.out.println(list.toString());
}
}
/* Output
[findingsea, findingsea, findingsea, findingsea, findingsea]
*/
代码片段同样展示的是泛型的内部一致性。
擦除的补偿
如上看到的,但凡是涉及到确切类型信息的操作,在泛型内部都是无法共工作的。那是否有办法绕过这个问题来编程,答案就是显示地传递类型标签。
public class Main<T> {
public T create(Class<T> type) {
try {
return type.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static void main(String[] args) {
Main<String> m = new Main<String>();
String s = m.create(String.class);
}
}
代码片段展示了一种用类型标签生成新对象的方法,但是这个办法很脆弱,因为这种办法要求对应的类型必须有默认构造函数,遇到Integer类型的时候就失败了,而且这个错误还不能在编译器捕获。
public class Main<T> {
public T[] create(Class<T> type) {
return (T[]) Array.newInstance(type, 10);
}
public static void main(String[] args) {
Main<String> m = new Main<String>();
String[] strings = m.create(String.class);
}
}
代码片段七展示了对泛型数组的擦除补偿,本质方法还是通过显示地传递类型标签,通过Array.newInstance(type,
Java 经典问题