线程安全:当多个线程访问某一个类(对象或方法)时,对象对应的公共数据区始终都能表现正确,那么这个类(对象或方法)就是线程安全的。
volatile 关键字的作用 保证内存的可见性 防止指令重排
注意:volatile 并不保证原子性
可见性原理
volatile 保证可见性的原理是在每次访问变量时都会进行一次刷新,因此每次访问都是主内存中最新的版本。所以 volatile 关键字的作用之一就是保证变量修改的实时可见性。
一个非常重要的问题,是每个学习、应用多线程的Java程序员都必须掌握的。理解volatile关键字的作用的前提是要理解Java内存模型,volatile关键字的作用主要有两个:
(1)多线程主要围绕可见性和原子性两个特性而展开,使用volatile关键字修饰的变量,保证了其在多线程之间的可见性,即每次读取到volatile变量,一定是最新的数据
(2)代码底层执行不像我们看到的高级语言----Java程序这么简单,它的执行是Java代码–>字节码–>根据字节码执行对应的C/C++代码–>C/C++代码被编译成汇编语言–>和硬件电路交互。现实中,为了获取更好的性能JVM可能会对指令进行重排序,多线程下可能会出现一些意想不到的问题。使用volatile则会对禁止语义重排序,当然这也一定程度上降低了代码执行效率。
从实践角度而言,volatile的一个重要作用就是和CAS结合,保证了原子性,详细的可以参见java.util.concurrent.atomic包下的类,比如AtomicInteger。
1、新建状态(New)
用new关键字和Thread类或其子类建立一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态(runnable)。
注意:不能对已经启动的线程再次调用start()方法,否则会出现java.lang.IllegalThreadStateException异常。
2、就绪状态(Runnable)
处于就绪状态的线程已经具备了运行条件,但还没有分配到CPU,处于线程就绪队列(尽管是采用队列形式,事实上,把它称为可运行池而不是可运行队列。因为cpu的调度不一定是按照先进先出的顺序来调度的),等待系统为其分配 CPU。等待状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会从等待执行状态进入执行状态,系统挑选的动作称之为“cpu调度”。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。
提示:如果希望子线程调用start()方法后立即执行,可以使用Thread.sleep()方式使主线程睡眠一伙儿,转去执行子线程。
3、运行状态(Running)
处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
处于就绪状态的线程,如果获得了cpu的调度,就会从就绪状态变为运行状态,执行run()方法中的任务。如果该线程失去了cpu资源,就会又从运行状态变为就绪状态。重新等待系统分配资源。也可以对在运行状态的线程调用yield()方法,它就会让出cpu资源,再次变为就绪状态。
当发生如下情况是,线程会从运行状态变为阻塞状态:
①、线程调用sleep方法主动放弃所占用的系统资源
②、线程调用一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
③、线程试图获得一个同步监视器,但更改同步监视器正被其他线程所持有
④、线程在等待某个通知(notify)
⑤、程序调用了线程的suspend方法将线程挂起。不过该方法容易导致死锁,所以程序应该尽量避免使用该方法。
当线程的run()方法执行完,或者被强制性地终止,例如出现异常,或者调用了stop()、desyory()方法等等,就会从运行状态转变为死亡状态。
4、阻塞状态(Blocked)
处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己的运行,进入阻塞状态。阻塞的情况分三种:
(一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。
(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁(synchronized)被别的线程占用,则JVM会把该线程放入锁池中。
(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续运行。有三种方法可以暂停Threads执行。
5、死亡状态(Dead)
需要说明的是,synchronized锁和调用wait()的对象应为同一对象!否则会报java.lang.IllegalMonitorStateException错误。

对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。
sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。
在调用sleep()方法的过程中,线程不会释放对象锁。
而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备
获取对象锁进入运行状态。
这个问题常问,sleep方法和wait方法都可以用来放弃CPU一定的时间,不同点在于如果线程持有某个对象的监视器,sleep方法不会放弃这个对象的监视器,wait方法会放弃这个对象的监视器
Sleep 接口均带有表示睡眠时间长度的参数 timeout。调用以上提到的 Sleep 接口,会有条件地将调用线程从当前处理器上移除,并且有可能将它从线程调度器的可运行队列中移除。这个条件取决于调用 Sleep 时timeout 参数。
当 timeout = 0, 即 Sleep(0),如果线程调度器的可运行队列中有大于或等于当前线程优先级的就绪线程存在,操作系统会将当前线程从处理器上移除,调度其他优先级高的就绪线程运行;如果可运行队列中的没有就绪线程或所有就绪线程的优先级均低于当前线程优先级,那么当前线程会继续执行,就像没有调用 Sleep(0)一样。
当 timeout > 0 时,如:Sleep(1),会引发线程上下文切换:调用线程会从线程调度器的可运行队列中被移除一段时间,这个时间段约等于 timeout 所指定的时间长度。为什么说约等于呢?是因为睡眠时间单位为毫秒,这与系统的时间精度有关。通常情况下,系统的时间精度为 10 ms,那么指定任意少于 10 ms但大于 0 ms 的睡眠时间,均会向上求值为 10 ms。
而调用 SwitchToThread() 方法,如果当前有其他就绪线程在线程调度器的可运行队列中,始终会让出一个时间切片给这些就绪线程,而不管就绪线程的优先级的高低与否。
如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:
1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
2)线程执行发生异常,此时JVM会让线程自动释放锁。
那么如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。
因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。
再举个例子:当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。
但是采用synchronized关键字来实现同步的话,就会导致一个问题:
如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。
因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。
另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。
总结一下,也就是说Lock提供了比synchronized更多的功能。但是要注意以下几点:
1)Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;
2)Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
使用独占锁机制来解决,是一种悲观的并发策略,抱着一副“总有刁民想害朕”的态势,每次操作数据的时候都认为别的线程会参与竞争修改,所以直接加锁。同一刻只能有一个线程持有锁,那其他线程就会阻塞。线程的挂起恢复会带来很大的性能开销,尽管jvm对于非竞争性的锁的获取和释放做了很多优化,但是一旦有多个线程竞争锁,频繁的阻塞唤醒,还是会有很大的性能开销的。所以,使用synchronized或其他重量级锁来处理显然不够合理。
乐观的解决方案,顾名思义,就是很大度乐观,每次操作数据的时候,都认为别的线程不会参与竞争修改,也不加锁。如果操作成功了那最好;如果失败了,比如中途确有别的线程进入并修改了数据(依赖于冲突检测),也不会阻塞,可以采取一些补偿机制,一般的策略就是反复重试。很显然,这种思想相比简单粗暴利用锁来保证同步要合理的多。
各种锁的的解释如下:
AtomicInteger,原理CAS,unSafe类。
以前创建一个线程都是使用new Thread();的方法。线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
主要特点:线程复用;控制最大并发数;管理线程。
优势:
一、降低资源消耗。通过重复利用已创建的线程来降低线程创建和销毁造成的消耗。
二、提高相应速度,当任务到达时,任务可以不需要的等到线程创建就能立即执行。
三、提高线程的可管理性,线程是稀缺资源,如果无限制的创建,不仅仅会消耗系统资源,还会降低体统的稳定性,使用线程可以进行统一分配,调优和监控。
线程池常用的三种实现如下:
newCache是创建可扩容的带有缓存的一池多线程
newFixed是创建固定个数的线程池
Shutdown():
1、调用之后不允许继续往线程池内继续添加线程;
2、线程池的状态变为SHUTDOWN状态;
3、所有在调用shutdown()方法之前提交到ExecutorSrvice的任务都会执行;
4、一旦所有线程结束执行当前任务,ExecutorService才会真正关闭。
shutdownNow():
1、该方法返回尚未执行的 task(任务) 的 List;
2、线程池的状态变为STOP状态;
3、阻止所有正在等待启动的任务, 并且停止当前正在执行的任务。
ScheduledThreadPoolExecutor 设置定时,进行调度,可以用来在给定延时后执行异步任务或者周期性执行任务。
public ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory) {
super(corePoolSize, Integer.MAX_VALUE, 0,TimeUnit.NANOSECONDS, new DelayedWorkQueue(),threadFactory);
}
单例,通过单例工厂 DefaultSingletonBeanRegistry实现单例;
AsyncTaskExecutor保证并发的安全。
public static void main(String[] args) {
final String str="abc";
ExecutorService executorService= Executors.newFixedThreadPool(3);
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println("1"+str);
}
});
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println("2"+str);
}
});
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println("3"+str);
}
});
}
对于链表这类的数据结构,无非就涉及到增删改查四种基本操作,我们只要保证在对链表进行增删改查时是线程安全的即可。在绝大多数情况下,读的操作多于增删改的操作,所以可以考虑使用JUC包下的读写锁类ReentrantReadWriteLock,当进行读操作时获取读锁,进行增删改操作时获取写锁。这样可以保证多个线程可以同时读取链表中的数据,某个时刻只有一个线程能对链表进行增删改。ReentrantReadWriteLock可以参考:
final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public void read(String params){
lock.readLock().lock();
try {
// read data
}finally {
lock.readLock().unlock();
}
}
public void add_delete_update(String params){
lock.writeLock().lock();
try {
// add or delete or update data
}finally {
lock.writeLock().unlock();
}
}
wait 会挂起自己让出 CPU 时间片,并将自身加入锁定对象的 Wait Set 中,释放对象的监视器锁(monitor)让其他线程可以获得,直到其他线程调用此对象的 notify( ) 方法或 notifyAll( ) 方法,自身才能被唤醒(这里有个特殊情况就是 Wait 可以增加等待时间);notify方法则会释放监视器锁的同时,唤醒对象 Wait Set 中等待的线程,顺序是随机的不确定。
根据具体情况(sleep,wait,join等),酌情选择notifyAll,notify进行线程唤醒。
countdowlatch:让一些线程堵塞直到另一个线程完成一系列操作后才被唤醒。CountDownLatch 主要有两个方法,当一个或多个线程调用 await 方法时,调用线程会被堵塞,其他线程调用 countDown 方法会将计数减一(调用 countDown 方法的线程不会堵塞),当计数其值变为零时,因调用 await 方法被堵塞的线程会被唤醒,继续执行。
类似生活场景:关门:等到商场里面所有的人都走了,才能够关门。
cyclicbarrier :与CountDownLatch相反,CyclicBarrier是增加到指定的数目才结束。
Synchronized修饰非静态方法,实际上是对调用该方法的对象加锁,俗称“对象锁”。
Java中每个对象都有一个锁,并且是唯一的。假设分配的一个对象空间,里面有多个方法,相当于空间里面有多个小房间,如果我们把所有的小房间都加锁,因为这个对象只有一把钥匙,因此同一时间只能有一个人打开一个小房间,然后用完了还回去,再由JVM 去分配下一个获得钥匙的人。
Synchronized修饰静态方法,实际上是对该类对象加锁,俗称“类锁”。
区别:
LinkedBlockingQueue 多用于任务队列
ConcurrentLinkedQueue 多用于消息队列
单生产者,单消费者 用 LinkedBlockingqueue
多生产者,单消费者 用 LinkedBlockingqueue
单生产者,多消费者 用 ConcurrentLinkedQueue
多生产者,多消费者 用 ConcurrentLinkedQueue
当线程 A 持有独占锁a,并尝试去获取独占锁 b 的同时,线程 B 持有独占锁 b,并尝试获取独占锁 a 的情况下,就会发生 AB 两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。
四个原因:
当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。
在有些情况下死锁是可以避免的。两种用于避免死锁的技术:
runnable 没有返回值,callable 可以拿到有返回值,callable 可以看作是 runnable 的补充。
Bit 位:是计算机最小的二进制单位 ,取0或1,主要用于计算机操作。
Byte 字节:是数据的最小单位,由8位bit组成,取值(-128 - 127),主要用于计算机操作数据。
Char 字符:是用户可读写的最小单位,由16位bit(2个byte)组成,取值(0-65535),主要用于用户操数数据。
它是指数据从源头流到目的地,所以常把这种数据流叫做IO流。
常用来处理设备之间的数据传输、文件的上传、下载、拷贝。
流分输入和输出,输入流从文件中读取数据存储到进程中,输出流从进程中读取数据然后写入到目标文件。

流按照传输单位分为 字节流和字符流
字节流的抽象基类(父类)是:java.io.InputStream、java.io.OutputStream
字符流的抽象基类(父类)是:java.io.Reader 、 java.io.Writer
面向字节的操作以8为为单位对二进制数据进行操作,对数据不需要进行转换,所有的类都是InputStream和OutputStream的子类(以InputStream和OutputStream为后缀)。
面向字符的操作以字符为单位对数据进行操作,在读取的时候将二进制数据转换成字符,在写的时候则是将字符转换成二进制数据,这些类都是Reader和Writer的子类(以Reader和Writer为后缀)。
常用的io流
IO类设计时使用了装饰者设计模式。
相对于内存(程序)来说,流按照传输方向,可以分为输入流InputStream、输出流OutputStream
按照功能分为:节点流OutputStream、处理流 OutputStreamWriter
节点流 :直接与数据源相连,用于输入或输出
处理流:在节点流的基础上对之进行加工,进行一些功能的扩展
处理流的构造器必须要传入节点流的子类
字符流属于处理流中的缓冲流,可以将读取的内容存在内存里面,有readLine()方法读取一行文本,从字符输入流中读取文本,缓冲各个字符,从而提供字符、数组和行的高效读取。
这种处理流的构造器需要传入字点流。
缓冲区就是一段特殊的内存区域,很多情况下当程序需要频繁地操作一个资源(如文件或数据库)则性能会很低,所以为了提升性能就可以将一部分数据暂时读写到缓存区,以后直接从此区域中读写数据即可,这样就显著提升了性。
对于 Java 字符流的操作都是在缓冲区操作的,所以如果我们想在字符流操作中主动将缓冲区刷新到文件则可以使用 flush() 方法操作。
字符流和字节流的使用非常相似,但是实际上字节流的操作不会经过缓冲区(内存)而是直接操作文本本身的,而字符流的操作会先经过缓冲区(内存)然后通过缓冲区再操作文件。
详细回答:
流一旦打开就必须关闭,使用close方法,放入finally语句块中(finally 语句一定会执行);
调用的处理流就关闭处理流,多个流互相调用只关闭最外层的流;
read() : 从输入流中读取数据的下一个字节,返回0到255范围内的int字节值。如果因为已经到达流末尾而没有可用的字节,则返回-1。在输入数据可用、检测到流末尾或者抛出异常前,此方法一直阻塞。
read(byte[] b) : 从输入流中读取一定数量的字节,并将其存储在缓冲区数组 b 中。以整数形式返回实际读取的字节数,到未尾时都返回-1。在输入数据可用、检测到文件末尾或者抛出异常前,此方法一直阻塞。
write()将指定字节传入数据源
Byte b[ ]是byte数组,b[off]是传入的第一个字符,b[off+len-1]是传入的最后的一个字符 ,len是实际长度
当我们new一个java流对象之后,不仅在计算机内存中创建了一个相应类的实例对象。而且,还占用了相应的系统资源,比如:文件句柄、端口、数据库连接等。在内存中的实例对象,当没有引用指向的时候,java垃圾收集器会按照相应的策略自动回收,但是却无法对系统资源进行释放。 所以,我们需要主动调用close()方法释放java流对象。在java7之后,可以使用try-with-resources语句来释放java流对象,从而避免了try-catch-finally语句的繁琐,尤其是在finally子句中,close()方法也会抛出异常。
集合是Java中的一个非常重要的一个知识点,主要分为List、Set、Queue三大数据结构。它们在Java中的结构关系如下:
Map接口有两个常用的实现类:Hashtable和HashMap。
要实现比较有两种方式:
第一种:实体类实现Comparable接口,并实现 compareTo(T t) 方法,我们称为内部比较器。
第二种:创建一个外部比较器,这个外部比较器要实现Comparator接口的 compare(T t1, T t2)。
如果你希望该实体类在放入集合的时候能按照你希望的方式排序(如果集合支持),那么你需要让实体类实现Comparable接口。如果你只是需要简单比较集合中实体类的大小,最后返回一个结果,那么用Comparator接口实现一个外部比较器更合适。
ArrayList和Vector都实现了List接口,他们都是有序集合,并且存放的元素是允许重复的。它们的底层都是通过数组来实现的,因此列表这种数据结构检索数据速度快,但增删改速度慢。
而ArrayList和Vector的区别主要在两个方面:
第一,线程安全。Vector是线程安全的,而ArrayList是线程不安全的。因此在如果集合数据只有单线程访问,那么使用ArrayList可以提高效率。而如果有多线程访问你的集合数据,那么就必须要用Vector,因为要保证数据安全。
第二,数据增长。ArrayList和Vector都有一个初始的容量大小,当存储进它们里面的元素超过了容量时,就需要增加它们的存储容量。ArrayList每次增长原来的0.5倍,而Vector增长原来的一倍。ArrayList和Vector都可以设置初始空间的大小,Vector还可以设置增长的空间大小,而ArrayList没有提供设置增长空间的方法。
对于在 Map 中插入、删除、定位一个元素这类操作,HashMap 是最好的选择,因为相对而言 HashMap 的插入会更快,但如果你要对一个 key 集合进行有序的遍历,那 TreeMap 是更好的选择。
HashMap 基于 Hash 算法实现的,我们通过 put(key,value)存储,get(key)来获取。当传入 key 时,HashMap 会根据 key. hashCode() 计算出 hash 值,根据 hash 值将 value 保存在 bucket 里。当计算出的 hash 值相同时,我们称之为 hash 冲突,HashMap 的做法是用链表和红黑树存储相同 hash 值的 value。当 hash 冲突的个数比较少时,使用链表,否则使用红黑树。
HashSet 是基于 HashMap 实现的,HashSet 底层使用 HashMap 来保存所有元素,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成,HashSet 不允许重复的值。
综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList。
数组大小固定,并且缺少很多api直接使用。集合可以动态扩容,并且封装了很多api使用。
相同点:都是返回第一个元素,并在队列中删除返回的对象。
不同点:如果没有元素 poll()会返回 null,而 remove()会直接抛出 NoSuchElementException 异常。
Vector、Hashtable、Stack 都是线程安全的,而像 HashMap 则是非线程安全的,不过在 JDK 1.5 之后随着 Java. util. concurrent 并发包的出现,它们也有了自己对应的线程安全类,比如 HashMap 对应的线程安全类就是 ConcurrentHashMap。
Iterator 接口提供遍历任何 Collection 的接口。我们可以从一个 Collection 中使用迭代器方法来获取迭代器实例。迭代器取代了 Java 集合框架中的 Enumeration,迭代器允许调用者在迭代过程中移除元素。
Iterator 的特点是更加安全,因为它可以确保,在当前遍历的集合元素被更改的时候,就会抛出 ConcurrentModificationException 异常。
Iterator 使用代码如下:
List<String> list = new ArrayList<>();
Iterator<String> it = list. iterator();
while(it. hasNext()){
String obj = it. next();
System. out. println(obj);
}
可以使用 Collections.unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出Java. lang. UnsupportedOperationException异常。
示例代码如下:
List<String> list = new ArrayList<>();
list. add("x");
Collection<String> clist = Collections.unmodifiableCollection(list);
clist. add("y"); // 运行时此行报错
System.out.println(list. size());
一方面是List和Map的数据结构,另一方面是存储数据的特点。在数据结构方面,List存储的是单列数据的集合,而Map存储的是key、value类型的数据集合。在数据存储方面,List存储的数据是有序且可以重复的,而Map中存储的数据是无序且key值不能重复(value值可以重复)。
List与Set具有相似性,它们都是单列元素的集合,所以,它们有一个功共同的父接口,叫Collection。Set里面不允许有重复的元素,所谓重复,即不能有两个相等(注意,不是仅仅是相同)的对象 ,即假设Set集合中有了一个A对象,现在我要向Set集合再存入一个B对象,但B对象与A对象equals相等,则B对象存储不进去。所以,Set集合的add方法有一个boolean的返回值,当集合中没有某个元素,此时add方法可成功加入该元素时,则返回true,当集合含有与某个元素equals相等的元素时,此时add方法无法加入该元素,返回结果为false。Set取元素时,没法说取第几个,只能以Iterator接口取得所有的元素,再逐一遍历各个元素。
List表示有先后顺序的集合, 注意,不是那种按年龄、按大小、按价格之类的排序。当我们多次调用add(Obj e)方法时,每次加入的对象就像火车站买票有排队顺序一样,按先来后到的顺序排序。有时候,也可以插队,即调用add(int index,Obj e)方法,就可以指定当前对象在集合中的存放位置。一个对象可以被反复存储进List中,每调用一次add方法,这个对象就被插入进集合中一次,其实,并不是把这个对象本身存储进了集合中,而是在集合中用一个索引变量指向这个对象,当这个对象被add多次时,即相当于集合中有多个索引指向了这个对象,如图x所示。List除了可以以Iterator接口取得所有的元素,再逐一遍历各个元素之外,还可以调用get(index i)来明确说明取第几个。
Map与List和Set不同,它是双列的集合,其中有put方法,定义如下:put(obj key,obj value),每次存储时,要存储一对key/value,不能存储重复的key,这个重复的规则也是按equals比较相等。取则可以根据key获得相应的value,即get(Object key)返回值为key 所对应的value。另外,也可以获得所有的key的结合(map.keySet()),还可以获得所有的value的结合(map.values()),还可以获得key和value组合成的Map.Entry对象的集合(map.entrySet())。
List 以特定次序来持有元素,可有重复元素。Set 无法拥有重复元素,内部排序。Map 保存key-value值,value可多值。
三者之间的区别,如下表:

Set中元素不可以重复,是无序的(这里的无序是指存入元素的先后顺序与输出元素的先后顺序不一致)
HashSet:
①内部的数据结构是哈希表,是线程不安全的。
②HashSet中保证集合中元素是唯一的方法:通过对象的hashCode和equals方法来完成对象唯一性的判断。如果对象的hashCode值不同,则不用判断equals方法,就直接存到HashSet中。
注意:如果元素要存到HashSet中,必须覆盖hashCode方法和equals方法。
③HashSet是哈希表实现的,HashSet中的元素是无序的。集合元素可以是null,但只能放入一个null。
HashSet要求放入的对象必须实现HashCode()方法,放入的对象是以hashcode码作为标识的,而具有相同内容的String对象,HashCode是一样的,所以放入的内容不能重复。但是同一个类的对象可以放入不同的实例。
TreeSet:
TreeSet是SortedSet接口的唯一实现类,TreeSet可以保证集合元素处于排序状态。TreeSet支持两种排序方式,自然排序和定制排序,其中自然排序是默认的排序方式,想TreeSet中加入的对象应该是同一个类的对象。
①可以对Set集合中的元素进行排序,是线程不安全的。
②TreeSet判断元素唯一性的方法是:根据比较方法的返回结果是否为0,如果是0,则是相同元素,如果不是0,是不同元素,存储。
③TreeSet对元素进行排序的方式:元素自身具备比较的功能,即自然排序,需要实现Comparable接口,并覆盖其compareTo方法。元素本身不具备比较功能的,则需要实现Comparator,并覆盖其compare方法(定制排序)。
④TreeSet是二叉树实现的,TreeSet中的数据是自动排好顺序的,不允许放入null值
总结:HashSet存放对象的时候判断对象是否相等,是根据对象的hashCode和equals方法。TreeSet存放对象时,是根据对象的compareTo方法比较两个对象是否相等,并进行比较。
Hashset和Treeset的使用场景
HashSet:哈希表是通过使用称为散列法的机制来存储信息的,元素并没有以某种特定的顺序来存放。
TreeSet:提供一个使用树结构存储set接口的实现(红黑树算法)。对象以升序顺序存储,访问和遍历的时间很快。
使用场景:HashSet是基于Hash算法实现的,其性能通常都优于TreeSet。我们通常都应该使用HashSet,在我们需要排序的功能时,我们才使用TreeSet。
TreeMap 和 TreeSet 是 Java Collection Framework 的两个重要成员,其中 TreeMap 是 Map 接口的常用实现类,而 TreeSet 是 Set 接口的常用实现类。虽然 TreeMap 和TreeSet 实现的接口规范不同,但 TreeSet 底层是通过 TreeMap 来实现的(如同HashSet底层是是通过HashMap来实现的一样),因此二者的实现方式完全一样。而 TreeMap 的实现就是红黑树算法
TreeSet和TreeMap的关系
与HashSet完全类似,TreeSet里面绝大部分方法都市直接调用TreeMap方法来实现的。
相同点:
TreeMap和TreeSet都是非同步集合,因此他们不能在多线程之间共享,不过可以使用方法Collections.synchroinzedMap()来实现同步
运行速度都要比Hash集合慢,他们内部对元素的操作时间复杂度为O(logN),而HashMap/HashSet则为O(1)。
TreeMap和TreeSet都是有序的集合,也就是说他们存储的值都是拍好序的。
不同点:
LinkedHashSet继承自HashSet,源码更少、更简单,唯一的区别是LinkedHashSet内部使用的是LinkHashMap。这样做的意义或者好处就是LinkedHashSet中的元素顺序是可以保证的,也就是说遍历序和插入序是一致的。
反射是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 Java 语言的反射机制。
反射是框架设计的灵魂。
在我们平时的项目开发过程中,基本上很少会直接使用到反射机制,但这不能说明反射机制没有用,实际上有很多设计、开发都与反射机制有关,例如模块化的开发,通过反射去调用对应的字节码;动态代理设计模式也采用了反射机制,还有我们日常使用的 Spring/Hibernate 等框架也大量使用到了反射机制。
举例:
①我们在使用JDBC连接数据库时使用Class.forName()通过反射加载数据库的驱动程序;
②Spring框架也用到很多反射机制,最经典的就是xml的配置模式。Spring 通过 XML 配置模式装载 Bean 的过程:
优点: 运行期类型的判断,动态加载类,提高代码灵活度。
缺点: 性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的java代码要慢很多。
public class Student {
private int id;
String name;
protected boolean sex;
public float score;
}
public class Get {
//获取反射机制三种方式
public static void main(String[] args) throws ClassNotFoundException {
//方式一(通过建立对象)
Student stu = new Student();
Class classobj1 = stu.getClass();
System.out.println(classobj1.getName());
//方式二(所在通过路径-相对路径)
Class classobj2 = Class.forName("com.kaico.entity.Student");
System.out.println(classobj2.getName());
//方式三(通过类名)
Class classobj3 = Student.class;
System.out.println(classobj3.getName());
}
}
简单来说序列化就是一种用来处理对象流的机制。所谓对象流也就是将对象的内容进行流化,流的概念这里不用多说(就是I/O)。我们可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间(注:要想将对象传输于网络必须进行流化)!在对对象流进行读写操作时会引发一些问题,而序列化机制正是用来解决这些问题的!
1、方便网络传输
我们都知道,socket的数据都是以字符串进行传输,而序列化的作用就是将复杂的数据结构转换成字符串。
2、方便协议解释
序列化中的“序”就是有序的意思,有序的字符串序列可以供绝大多数的编程语言解释。而Protocol Buffers就是其中的突出代表。
3、方便数据存储
方便数据存储这个好理解,就是解决一对多的问题。不用序列化可能需要存储多条信息,而序列化可以将多条数据合成一条进行存储;方便数据运维。
序列化也是一把双刃剑,优点明显,缺点同样明显!
(1)Java序列化就是指把Java对象转换为字节序列的过程
Java反序列化就是指把字节序列恢复为Java对象的过程。
(2)序列化最重要的作用:在传递和保存对象时.保证对象的完整性和可传递性。对象转换为有序字节流,以便在网络上传输或者保存在本地文件中。
反序列化的最重要的作用:根据字节流中保存的对象状态及描述信息,通过反序列化重建对象。
总结:核心作用就是对象状态的保存和重建。(整个过程核心点就是字节流中所保存的对象状态及描述信息)
serialVersionUID 是 Java 为每个序列化类产生的版本标识,可用来保证在反序列时,发送方发送的和接受方接收的是可兼容的对象。如果接收方接收的类的 serialVersionUID 与发送方发送的 serialVersionUID 不一致,进行反序列时会抛出 InvalidClassException。序列化的类可显式声明 serialVersionUID 的值。
serialVersionUID 是 Java 为每个序列化类产生的版本标识,可用来保证在反序列时,发送方发送的和接受方接收的是可兼容的对象。如果接收方接收的类的 serialVersionUID 与发送方发送的 serialVersionUID 不一致,进行反序列时会抛出 InvalidClassException。序列化的类可显式声明 serialVersionUID 的值。
目的在在加快程序开发的效率。通过使用它开放出来的注解api,你几乎可以使用在任何地方, 大大的减少了无关痛痒的代码量,让开发者能够抽身其外,有足够的时间精力关注在真正的业务逻辑上面。而且通过简洁你的代码,也提高了代码的稳定性和后期的维护成本。
通过反射机制实现的。通过在Runtime运行期去反射类中带有注解的Field和Method,然后再去执行注解相对应的逻辑代码。
首先,不会影响执行效率。
原因:在编译器中加了一层额外的自动编译步骤,用来生成基于你源码的代码。生成的代码是你源码的直接子类,而且自动生成的类的名称就是父类名称后面加个下划线。