文章目录
前言
当对OOP的规则感到迷惑的时候,请先将你操作过的最大项目的代码量扩大100倍,再思考这些规则的合理性
从进阶部分开始出现许多API,像类名一般以or结尾(取服务提供者之意),接口名以able结尾(取功能之意)
一. 多线程
1. 基本概念
在OS中已经了解过程序,进程,线程
程序:一组指令的集合。之前OOP还说程序是对象的集合
进程:正在运行的一个程序。也是资源分配的单位,系统会为进程分配不同的内存区域。可以再任务管理器中看
线程:程序的一条执行路径(很抽象)。比如说main就是一个线程,是程序的一条执行路径。如果程序有多条执行路径(并发,不是并行,默认单处理器)
并发:一个处理器同时处理多个任务,并且只是逻辑上的同时处理(看起来是),主要是有些任务开销大,CPU空闲
并行:多个处理器同时处理多个任务,这就是物理上的同时处理
线程是资源调度和执行的单位,切换线程的开销小
共享进程的资源,线程从同一个堆中分配对象
但是共享就会带来冲突和安全隐患(线程同步)
方法区和堆,一个进程一份
虚拟机栈和PC,一个线程一份
一个java应用程序,至少3个线程,main,垃圾回收,异常处理
考虑这样一个场景,在单核的背景下,使用单个线程完成多个任务快还是使用多个线程更快??
显然是单个线程更快,那么为什么还要使用线程呢??
(1)对图形化界面很必要,即便慢一点,但用户体验更好
(2)提高CPU的利用率,虽然有切换线程的开销
(3)利于程序的解耦
何时创建线程??
(1)程序需要同时执行多个任务,比如main调用多个不同的方法
(2)需要执行需要等待的任务。比如等待用户输入,等待联网等等。比如手机淘宝,往下划的时候,会显示新的内容,新内容的填充就是通过线程来做的。
(3)一些后台运行的程序
2. 线程的创建和使用
怎么判断程序是不是多线程??看程序的执行路径是不是一棵单根树,能不能用一根连续的向量串起来
通过java.lang.Thread类来创建多线程程序
法一:继承Thread类
(1)创建子类
(2)重写Thread类的run方法,将此线程会执行的操作声明在run方法中。相当于一组操作一个线程
(3)创建子类对象
(4)通过子类对象调用start方法。启动线程,并调用当前线程的run方法
class Window1 extends Thread{
private static int num;
public void run(){
while(true){
if(num <= 100){
System.out.println(this.getName() + ":" + num);
num++;
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Window1 w1 = new Window1();
Window1 w2 = new Window1();
w1.start();
w2.start();
}
}
如果子类对象不调用run方法,那要是里边的操作需要外边传入的实参怎么办呢???
(1)声明子类的属性
如果在子类对象调用start方法之后,再加一条语句,比如输出,这条输出指令就是由main这个主线程执行的。假设子类的run是做1-100数字的输出,请问这两个线程的执行顺序??
由main执行的输出结果可能会插入到子类线程的输出结果,其实从这就能看到线程安全与否这个问题
必须使用子类对象的start方法启动线程,如果直接用子类对象调用run方法,相当于是在main线程中的对象调用了一个方法而已
Thread类的常用方法
yield方法:释放当前cpu的执行权,主动切换线程
join方法:不可控时间的阻塞。在线程a中调用线稳b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程才结束阻塞状态。将调用join的线程的run方法执行完,执行完之前不再进行线程切换。
stop方法:强制结束当前线程
sleep方法:形参为睡眠时间,以毫秒为单位。可控时间的阻塞
isAlive方法:判断当前线程是否存活。
currentThread方法:一开始感觉没必要,获取当前线程的引用,然后就可以调用其他的方法。假如要给main这个主 线程重设线程名呢???难道是main.setName(),main是方法名啊,它是主线程对象的引用吗???至少这个时候currentThread方法还是有用的。还有就是如果是以实现runable接口来创建线程的话,在runable接口的实现类里边想要得到线程名,没有更好的办法了
线程调度方式
策略:时间片(均匀),抢占式(设置优先级队列)
java的调度方案-混合式
(1)同优先级,时间片
(2)不同优先级,抢占式
优先级1-10,默认是5
优先级方法:
(1)getPriority方法:返回当前线程的优先级
(2)setPriority方法:设置线程的优先级,传入想设置的优先级等级,整型优先级
说明:高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只有当高优先级的线程执行完以后,低优先级的线程才执行。
法二 实现Runable接口
接口:全局常量+抽象方法,静态方法,默认方法
(1)创建实现了runable接口的类
(2)实现runable中的抽象方法run方法
(3)创建实现类的对象
(4)将此对象作为参数传入Thread类的构造器,创建Thread类的对象
(5)通过Thread类的对象调用start方法
class Window implements Runnable{
private static int num;
public void run(){
while(true){
if(num <= 100){
System.out.println(Thread.currentThread().getName() + ":" + num);
num++;
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Window p = new Window();
Thread t1 = new Thread(p);
Thread t2 = new Thread(p);
t1.start();
t2.start();
}
}
线程的两种创建方式的比较
在实际开发中,使用第二种方法更好。因为我们希望项目中的继承树都是有意义的,一个类有他自己的状态和行为,为了使用线程,就说他是一个线程感觉上不太好。但是实现接口,无非就是让他多了一个功能,相对来说更符合OOP的逻辑上的规范。还有一个是出于共享数据的需要
联系:Thread类也是实现了Runable接口的。
3. 线程的生命周期
用Thread.State类定义线程的集中状态
4. 线程同步
讨论这个问题,可以先说说线程的切换发生在什么时候???
A:每个线程都有自己需要执行的一系列指令。线程切换发生的位置可能是在一系列指令中间的某个位置。很明显,从中间某个位置切换有极大可能造成前后结果不一致
对共享数据的操作
操作:增删改查
当使用多个线程操作共享数据,线程的切换会造成操作的不完整性,这种不完整带来的不确定性就导致程序执行结果的不稳定
解决方法:锁定共享资源,当一个线程进行与共享资源相关的操作时,锁定共享资源,以至于其他线程无法访问共享资源,直到当前的线程操作结束
同步机制
(1)同步代码块
synchronized(同步监视器){
//需要被同步的代码
}
如果线程是通过继承创建的,那么同步代码块如下:
class Window1 extends Thread{
private static int num;
private static Object obj = new Object();//区别就在这个静态对象,设置为同步锁
public void run(){
while(true){
synchronized(obj){
if(num <= 100){
System.out.println(this.getName() + ":" + num);
num++;
}
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Window1 w1 = new Window1();
Window1 w2 = new Window1();
w1.start();
w2.start();
}
}
如果线程是通过实现接口创建的,那么同步代码快如下:
class Window implements Runnable{
private static int num;
public void run(){
while(true){
synchronized(this){
if(num <= 100){
System.out.println(this.getName() + ":" + num);
num++;
}else{
break;
}
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Window w1 = new Window();
Window w2 = new Window();
w1.start();
w2.start();
}
}
说明:操作共享资源的代码,即为需要被同步的代码
同步监视器俗称同步锁,任何一个类的对象都可以充当锁,new一个Object对象就行了。但是要保证对象的
唯一性。如果当前对象唯一,也可以用this;另外类也是对象,因为类只加载一次,所以类这个对象是唯一
的,类名.class就是类对象。this用在实现runable接口,类名.class用在继承Thread类
线程同步解决了线程安全问题,但是操作同步代码,只能有一个线程参与,相当于是一个单线程的过程
(2)同步方法
如果操作共享资源的代码完整地声明在一个方法中,那么就将此方法声明为同步方法
public synchronized void run(){}
如果线程通过继承实现
class Window1 extends Thread{
private static int num;
private static Object obj = new Object();
public void run(){
while(true){
this.show();
}
}
private static synchronized void show(){
if(num <= 100){
System.out.println(Thread.currentThread().getName() + ":" + num);
num++;
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Window1 w1 = new Window1();
Window1 w2 = new Window1();
w1.start();
w2.start();
}
}
如果线程通过实现接口创建
class Window2 implements Runnable{
private static int num;
public void run(){
while(true){
this.show();
}
}
private synchronized void show(){
if(num <= 100){
System.out.println(Thread.currentThread().getName() + ":" + num);
num++;
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Window2 p = new Window2();
Thread t1 = new Thread(p);
Thread t2 = new Thread(p);
t1.start();
t2.start();
}
}
同步锁:
对于实现runable接口,同步锁默认是当前对象this
对于继承thread类,将同步方法声明为静态方法,同步锁默认为类名.class(我感觉那就不需要声明静态方法把)
死锁
不同的线程一方面占用对方的资源,一方面等待对方放弃自己需要的资源,就形成了死锁。所有线程都被阻塞
共享资源不止一种了
专门的算法规避
避免同步嵌套
减少共享资源
(3)Lock锁接口
从jdk5,通过显式 定 义同步锁对象来实现同步。
增加了一个接口,java.util.concurrent.locks.Lock接口,用于控制多个线程对共享资源的访问
增加了一个实现类,ReentrantLock(可重入的锁),可以显式加锁,释放锁
reentrantlock类有两种构造器,无参和带bool fair的带参构造器 ,fair表示不会让某个线程连续分配CPU资源
操作:
(1)实例化reentrantlock类
(2)将操作共享资源的代码放到try的花括号中
(3)在代码之前,使用reentrantlock对象调用lock方法上锁
(4)没有catch
(5)在finally的花括号中,使用reentrantlock对象调用unlock方法解锁
如果线程由继承创建,那么lock代码块如下:
class Window1 extends Thread{
private static int num;
private static ReentrantLock lock = new ReentrantLock();// 可重入锁对象必须唯一
public void run(){
while(true){
try{
lock.lock();
if(num <= 100){
System.out.println(Thread.currentThread().getName() + ":" + num);
num++;
}else{
break;
}
}finally{
lock.unlock();
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Window1 w1 = new Window1();
Window1 w2 = new Window1();
w1.start();
w2.start();
}
如果线程由实现接口创建,那么lock代码块如下:
class Window2 implements Runnable{
private static int num;
private ReentrantLock lock = new ReentrantLock();//需要创建一个reentrantlock对象
public void run(){
while(true){
try{
lock.lock();
if(num <= 100){
System.out.println(Thread.currentThread().getName() + ":" + num);
num++;
}else{
break;
}
}finally{
lock.unlock();
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Window2 p = new Window2();
Thread t1 = new Thread(p);
Thread t2 = new Thread(p);
t1.start();
t2.start();
}
}
synchronized和Lock锁的异同???
相同点:都能解决线程安全
不同点:前者在执行完同步代码以后,自动释放同步监视器(同步锁);后者需要手动的上锁和解锁,但是比较灵活
5. 线程通信
线程之间不再只是独立地对共享资源进行操作。之前每个线程的执行是内核确定的,分好的时间片。但是现在可以手动控制每个线程同步代码的执行次数。比如说两个打印线程交替进行打印
这里发现我对前面部分的理解有问题,拿到同步监视器并不是说共享资源被锁住了,而是说同步代码执行完之前线程是不能切换的。也不会发生线程阻塞。只是说保证了操作同步资源的只有一个线程
释放同步锁,意味着线程该切换了,当前线程的状态可以是阻塞,就绪或者结束
前面不是讲了线程的生命周期中的5个状态吗,就绪和阻塞分别有一个队列让线程去排队
wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的
notifyAll():一旦执行此方法,就会唤醒所有被阻塞的线程。
说明:
(1)wait方法,notify方法,notifyall方法只能在同步代码块和同步方法中(不能在Lock代码块,lock代码块实现通信有其他方法)
(2)这3个方法都是通过同步代码块或者同步方法的同步监视器来调用的,否则会出现非法监视异常
(3)这3个方法定义在java.lang.Object类中,主要是因为任何一个对象都可以充当同步监视器
同步代码块的线程通信
class Window2 implements Runnable{
private static int num;
public void run(){
while(true){
synchronized(this){
notify();
if(num <= 100){
System.out.println(Thread.currentThread().getName() + ":" + num);
num++;
try {
wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}else{
break;
}
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Window2 p = new Window2();
Thread t1 = new Thread(p);
Thread t2 = new Thread(p);
t1.start();
t2.start();
}
}
注意:
(1)yield方法只是释放了同步锁,线程被切换,并没有使线程阻塞,被切换的线程有可能又被选上
(2)sleep方法和wait方法的异同
相同点:都可使线程阻塞
不同点:两者的声明位置不同,sleep声明在Thread类(由线程调用),wait声明在Object类(因为由同步监视器调用)
调用的范围不同:sleep可以在任何需要的场景调用,wait只能在同步代码块或同步方法中调用
是否释放同步监视器:sleep不会释放锁(线程不能被切换,虽然线程被阻塞了),wait阻塞了当前线程,将同步锁释放,此时线程一定被切换
生产者消费者问题
(1)是否是多线程问题?生产者、消费者线程
(2)是否存在安全问题(是否存在共享资源):产品的数量
(3)解决线程安全的方案?同步代码块,同步方法,Lock代码块
(4)是否存在通信(线程是否会被阻塞,怎样阻塞)?:三个方法的应用
6. jdk5新增线程创建方式
法三 实现Callable接口
相比Runable,Callable接口更强大
可以有返回值:那就可以进行线程之间的信息交互
方法可以抛出异常
支持泛型的返回值
需要借助FutureTask类,比如获取返回结果
class Num implements Callable{
public Object call() throws Exception{
int sum = 0;
for(int i = 1;i <= 100;i++){
sum+=i;
}
return sum;
}
}
public ThreadNew{
public static void main(String args[]){
Num num = new Num();
FutureTask ft = new FutureTask(num);
Thread t = new Thread(ft);
ft.start();
try{
//get()返回值即为FutureTask构造器参数calLable实现类重写的cal()的返回值。
Object sum = ft.get();
syso(sum);
}catch(InterruptedException e){e.printStackTrace();}
catch(ExecutionException e){e.printStackTrace();}
}
}
之所以以这种方式,可能就是为了解决runable哪里没有返回值的问题
法四 使用线程池
背景:频繁创建、销毁、使用数量巨大的资源,比如说线程,性能开销很大
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
好处:
(1)提高速度(因为提前造好了)
(2)降低资源消耗(已经造好的可以重复利用)
(3)便于管理:确定好一些用于管理的属性,核心池大小,最大线程数,保持时间
class NumThread implements Runnable{
public void run(){
for(100){
syso(i);
}
}
}
public class ThreadPoolTest{
main{
ExecutorService service = Executors.newFixedThreadPool(10);
//线程池属性的设置
//service的类型是ThreadPoolExecutor,这个类又是ExecutorService这个接口的实现类
ThreadPoolExecutor service1 = (ThreadPoolExecutor)service;
//这样就可以设置属性了
service.execute(new NumThread());
service.shutdown();
}
}
二. java常用类
1. 字符串相关类
2. jdk8之前的日期时间API
3. jdk8之后的日期时间API
三. java集合(容器,数据传输,什么类型的数据用哪种集合存储)
前言
在称呼的时候,可以说List容器(有序可重复容器),Set容器(无序不可重复容器),Map容器(键值对容器)
1. 集合框架概述
集合与数组是并列关系,都是容器。此时的存储,都是指内存存储,不涉及持久化存储(IO流)
集合的底层仍然是用数组实现的
数组的特点:长度不可变,元素类型也不可变(可以创建一个Object类数组);长度不可变不方便,数组提供的方法属性比较少,增删改查都没有;顺序表,有序可重复,对于无序不可重复的需求不能满足(哈希表);获取数组中实际元素的个数不方便(往往是单独定义一个变量total来统计)
Java集合可分为Collection和 Map 两种体系
集合API位于java.util包中
Collection接口:单列数据,定义了存取一组对象的方法的集合(就是一个一个的存)
List接口:元素有序、可重复的集合。一般用于替代之前使用的数组,但是List是动态数组(长度可变)
Set接口:元素无序、不可重复的集合。高中讲的那种集合。做重复数据的过滤
Map接口:双列数据,保存具有映射关系“key-value对”的集合(两个一组的存,key-value为一组)哈希表。函数。x和y是一一对应的,但反过来不一定
只有类和接口是实现关系,类和类,接口和接口都是继承关系。上图实线为继承,虚线为实现
所以vector, arraylist, linkedlist;hashset, linkedhashset, treeset都是类
2. Collection接口方法
vector, arraylist, linkedlist;hashset, linkedhashset, treeset
单列集合接口中的抽象方法,全局常量,静态方法,默认方法(规范)
常用方法
这里其实就看到了,为了解决接收不同类型对象的需求,add的是Object类对象
Collection coll = new ArrayList();//多态
coll.add(Object e);
coll.size();//集合的长度,反映的是数据的长度,而非数组的长度,注意区分
coll.addAll(Collection coll1);
coll.isEmpty();
coll.clear();
coll.contains(Object obj);//相当于按值查找,返回布尔值
coll.containsAll(Collection coll2);//刻画两个集合是否是包含关系
coll.remove(Object obj);//按值移除,返回布尔值,显示是否移除成功
coll.removeAll(Collection coll3);
coll.retainAll(Collection coll4);//返回coll和coll4的交集,赋值给coll
coll.equals(Collection coll5);//比较两个集合是否相同,比较内容,返回布尔值
coll.hashCode();//计算哈希值并返回,如果没有重写那就调用Object类中的(随机值)
coll.toArray();//返回Object类数组,实现集合到数组的转换
List list = Arrays.asList(new String[]{"aa", "bb"});//数组->集合,多态//asList()是个静态方法
List list2 = Arrays.asList(new int[]{1,2,3});//使用这种方法会被视作一个元素,改为
List list2 = Arrays.asList(new Integer[]{1,2,3});
iterator():返回Iterator接口的实例,用于遍历集合元素
coll.contains(Object obj):其中的值是如何比较的呢???
A:默认是调用Object类中的equals(),这个方法采用的就是==。String类已经重写了equals方法,因此他比较的是具体的内容。对于自定义类,如果没有重写equals,那就是比较引用;如果希望比较的是两个对象的属性,就需要重写equals方法
结论就是如果要装自定义类对象,都得重写equals方法
coll.remove(Object obj):为了返回布尔值,也调用了equals方法
打印集合对象:调用了每个对象的toString方法
coll.toArray();//从集合到数组
List list = Arrays.asList(new String[]{“aa”, “bb”});//数组->集合,List是接口,多态
List list2 = Arrays.asList(new int[]{1,2,3});//使用这种方法会被视作一个元素,改为
List list2 = Arrays.asList(new Integer[]{1,2,3});
我猜想,是因为集合的元素都是对象,而数组中的元素包括基本数据类型,在从数组到集合的过程,基本数据类型又没有自动装箱,因此最终转换到集合的只有基本数据类型数组的引用
3. Iterator迭代器接口
GOF给迭代器模式的定义为:提供一种方法访问一个容器(container)对象中各个元素,而又不需暴露该对象的内部细节。迭代器模式,就是为容器而生。类似于“公交车上的售票员”、“火车上的乘务员”、“空姐”。
iterator():返回Iterator接口的实例,用于遍历集合元素
Iterator iterator = coll.iterator();//感觉上是获取了集合的首地址,而集合本身遍历元素的功能被整合到了Iterator接口
iterator.next();//返回下一个元素,如果越界就跑无元素异常
//遍历集合
while(iterator.hasNext()){}//hasNext()返回布尔值是否还存在元素,底层调的是next()
while(iterator.hasNext()){
Object obj = iterator.next();
if("tom".equals(obj)){
iterator.remove();//移除集合中的元素,按值删除。但是集合实现类不是有//remove()吗
}
}
iterator.next():每使用一次,指针就后移一次
那就是说每次遍历集合,都需要创建一个新的迭代器对象,
Iterator iterator = coll.iterator()
iterator.remove(): 移除当前指针指向的集合元素,最开始指针指向的是集合首地址的上一个地址,也不能连续两次调用remove方法
foreach循环
jdk5提供了foreach循环迭代访问集合和数组
无需集合的长度,也无需索引访问元素
实际上,底层还是调用Iterator对象来完成操作
for(Object obj:coll){syso(obj);}
这种方式和python就很像了,但java是强类型语言,必须声明类型
a = list()
for obj in a:
print(a)
4. Collection子接口一:List
容器特点:有序可重复,是对普通数组的替换,长度可变,动态数组。注意,实现类都是长度可变
实现类:ArrayList、LinkedList和Vector
三种实现类的异同:
相同点:都实现了List接口,存储数据的特点相同
不同点:
(1)ArrayList:作为List接口的主要实现类,线程不安全(效率高),底层使用Object数组,那么这个容器就像python的列表。至于线程安全问题,Collections工具类可以处理
(2)LinkedList:底层使用双向链表。对于插入删除效率高(不需要移动元素)
(3)Vector:一种古老的实现类,在jdk1.0出现,不太用了,线程安全
源码分析
ArrayList实现类
如何实现自动扩容???
jdk7背景下:
(1)创建Object类数组elementData,并初始化长度为10
(2)添加元素之前,以size+1作为添加元素之后的数组长度去和初始化长度作差
(3)如果size+1 > 10, 那么就要进行扩容
(4)int newCapacity = oldcapacity + (oldCapacity >>1);
默认先扩容为原来的1.5倍
(5)newCapacity = size + 1;
如果还是小了,扩容为添加元素之后的数组长度
(6)事先设置了一个容器容量的上限,如果扩容后的容量>这个上限,就将容量扩容为整型数据的上限值
(8)最后将一个容量为扩容后大小的数组赋值给elementData
为了避免扩容,可以调用ArrayList类的带参构造器,初始化一个合适的容量
jdk8背景下:
(1)private static final 0bject[] DEFAULTCAPACITY_EMPTY_ELENENTDATA = {};
不像之前,从一开始就将数组实例化,只有一个引用。节省内存
(2)等到第一次添加元素的时候,比较默认容量(10)和size+1的大小,选择更大的那个实例化数组
操作上7和8没区别,只是底层变了
LinkedList实现类
双向链表,注意不是循环链表,尾结点不能直接回到头结点
节点first,last都是属性,表示头尾结点
这是节点的声明
添加元素:双向链表添加,不是直接添加就完了。
List接口常用方法
List除了从Collection集合继承的方法外,List集合里添加了一些根据索引来操作集合元素的方法。(因为Collection接口需要兼顾Set接口关于无序不可重复的要求,所以不应该设置根据索引操作元素的方法,只有List集合才有这个需求)
void add(Object obj);
void add(int index, 0bject ele);//在index位置插入ele元素,index从0开始计数
boolean addAll(int index, Collection eles);//从index位置开始将eles中的所有元素添加进来
Object get(int index)∶获取指定index位置的元素
indexOf(Object obj):返回obj在集合中首次出现的位置,如果没有返回-1
int lastIndex0f(Object obj):返回obj在当前集合中末次出现的位置
Object renbve(int index):移除指定index位置的元素,并返回此元素
object set(int index, object ele):设置指定index位置的元素为eLe
List sublist(int fromIndex, int toIndex):返回从fromIndex到toIndex位置的子集合,左闭右开区间
这里又遇到了List list1 = Arrays.asList(1,2,3);,集合里面装的都是对象,asList()传入的是一个整型数组的话,相当于传入了一个对象;如果传入的是一个Integer类数组的话,里面的每个元素都是对象
List遍历
lterator迭代器方式
增强for循环
普通的循环(因为List有索引访问了)
5. Collection子接口二:Set
特点:无序可重复,没有提供额外的方法
三种实现类:hashset, linkedhashset, treeset
hashSet: 最为Set接口的主要实现类,线程不安全,可以存储null值。底层也是数组,初始化长度为16
linkedhashset:hashSet的子类,遍历的时候按照添加顺序输出
treeset:使用红黑树存储。元素要求是同类型的。可以按照对象的指定属性进行排序
怎么理解无序性和不可重复?????
A:无序性不等于随机性。在代码层面,每次输出的顺序虽然和添加顺序不同,但输出顺序都是相同的。无序性指的是内存层面,存储的数据在底层数组中并非按照数组索引的顺序添加,而是按照哈希值。
不可重复性:保证添加的元素按照equals()判断时,不能返回true.即:相同的元素只能添加一个
hashSet
添加元素的过程
我们向HashSet中添加元素a,首先调用元素α所在类的hashCode()方法,计算元素a的哈希值,此哈希值接着通过某种算法计算出在HashSet底层数组中的存放位置(即为:索引位置)判断数组此位置上是否已经有元素:
如果此位置上没有其他元素,则元素α添加成功。
如果此位置上有其他元素b(或以链表形式存在的多个元素),则比较元素a与元素b的hash值:
如果hash值不相同,则元素α添加成功。
如果hash值相同,进而需要调用元素α所在类的equlas()方法:
返回true,添加失败
返回false,添加成功
对于出现占位且添加成功的情况而言:元素a与已经存在指定索引位置上数据以链表的方式存储
jdk 7 :元素α放到数组中,指向原来的元素。
jdk 8 ∶原来的元素在数组中,指向元素a
这种方式确实没有想到过,包括说从哈希值计算出索引位置,set集合以数组+链表的形式存在
我们的任务是筛出相同的对象–
哈希值不同,则对象的内容一定不同:先把哈希值映射到索引上面,索引可能冲突,再比较哈希值
相同的对象,哈希值一定相同
不同的对象,哈希值可能相同
Set集合添加元素用到的hashCode方法和equals方法的重写
可以看到哈希值是通过对象的属性来计算的,在上例中,对象只有两个属性,name是String类,age是整型
像Set集合中添加的自定义类,一定要重写equals方法和hashCode方法,需要保证相等对象,哈希值一定相等
LinkedHashSet实现类
内存结构:linkedhashset是hashset的子类,他继承了父类添加元素的方式,但同时每个元素都要维护两个指针,记录前驱和后继
注意啊,set集合是数组+链表的结构,别忘了
对于频繁的遍历,linkedhashset效率更高。主要是因为集合的遍历依靠的是指针的移动,数组+链表的结构导致这个结构不够平滑,后者因为记录了前驱和后继,在指针的移动上更加迅速
TreeSet实现类
特点:可以按照对象的指定属性,进行排序。意味着只能容纳同类型的对象。底层是红黑树
红黑树
在添加元素的过程就已经在排序,也就是说treeset容器总是有序的
自然排序和定制排序
自定义类需要去重写排序的逻辑
因为treeset集合总是有序,所以排序的过程就像是一趟插入排序,将添加元素插入有序的序列。排序就是不听比较两个数的大小
自然排序中,比较两个对象是否相同的标准为: compareTo()返回e.不再是equals().
自然排序:
(1)自定义类实现Comparable接口
(2)重写compareTo方法
看到这棵树就理解了许多,查询速度比List集合更快,这不废话吗,二分查找
定制排序:比较两个对象是否相同的标准为: compare()返回0.不再是equals().
Comparator类
去重的区别
前面的hashset,linkedhashset最后都是看equals方法的结果,但是treeset看的是compareTo方法的结果,因此重写的compareTo方法必须考虑相关的所有属性
6. Map接口
双列数据,存储键值对。类似于函数
Map 中的 key 用Set来存放,不允许重复,即同一个 Map 对象所对应的类,须重写hashCode()和equals()方法
这里有个反直觉的事情,为什么前面存储单列数据的容器也可以将hashxxx???
能不能用哈希,主要在于是否计算了哈希值
五个实现类:
hashtable:古老的实现类,线程安全
properties:常用来处理配置文件,键值都是String类型
hashmap :作为Map的主要实现类,线程不安全;可以存储null的key和value。
jdk7及之前:数组+链表 之后:数组+链表+红黑树
linkedhashmap:hashmap的子类,保证在遍历map元素时,可以按照添加的顺序实现遍历。在原有的HashMap底层结构基础上,添加了一对指针,指向前驱和后继。对于频繁的遍历,使用linked
treemap:类似于treeset,底层使用红黑树。可以按照添加的键值对进行排序,以key为依据排序
CurrentHashMap和Hashtable的异同???
A:前者实现了分段锁。为了避免线程等待,将线程要处理的文件分段,以保证线程总体的利用率
Map结构的理解
Map中的key:无序的、不可重复的,使用Set存储所有的key。因此,key所在的自定义类要重写equals方法和hashCode方法(以HashMap为例)
Map中的value:无序的、可重复的,使用collection存储所有的value***(为什么不同样使用Set)***
因此value所在的自定义类要重写equals方法
在HashMap的源码中,key和value的声明:
transient Set<K> keySet;
transient Collection<V> values;
一个键值对: key-value构成了一个Entry对象。
Map中的entry:无序的、不可重复的,使用Set存储所有的entry
HashMap的实现类的底层实现-jdk7
HashSet的底层就是用HashMap实现,前者的数据放到key的位置,value的位置为PRESENT = new Object()并且设置为静态
增加新元素
HashMap map = new HashMap();//在实例化以后,底层创建了长度是16的一维数组Entry[] table
....已经执行过多次put.....
map.put(key1, value1);
首先,调用key1所在类的hashcode()计算key1哈希值,此哈希值经过某种算法计算以后,得到在Entry数组中的存放位置,也就是索引。
(1)如果此位置上的数据为空,此时的key1-value1添加成功。
(2)如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),
比较key1和已经存在的一个或多个数据的哈希值:
(1)如果key1的哈希值与已经存在的数据的哈希值都不相同,此时key1-value1添加成功。
(2)如果key1的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同,继续比较:
调用key1所在类的equals(key2)方法,比较:
(1)如果equals()返回false:此时key1-value1添加成功。
(2)如果equals( )返回true:使用uaLue1替换相同key的value值。
这一套逻辑整体上和HashSet是一致的。不同的点在与,HashMap没有添加失败,当key重复了,会替换掉原来的value,更新成最新添加的value
结构上看起来是数组+链表
链表的新元素的添加:jdk7新元素放到数组,旧元素作为新元素的后继
根据前面的描述,我猜想entry数组的元素应该是两个,一个是Set容器,一个是Collection容器
扩容方式
当超出临界值,且新元素存放的位置非空,那么默认扩容为原来的两倍,并将原来的元素复制到新的数组中
jdk8
new HashMap():底层没有创建一个长度为16的数组
jdk8底层的数组是:Node[],而非Entry[]
首次调用put()方法时,底层创建长度为16的数组
jdk7底层结构只有:数组+链表。jdk8中底层结构:数组+链表+红黑树。
当数组的某一个索引位置上的元素以链表形式存在的数据个数>8且当前数组的长度>64时,此时此索引位置上的所有数据改为使用红黑树存储。
DEFAUL T_INITIAL_CAPACITY : HashMap的默认容量,16DEFAULT_LOAD_FACTOR: HashMap的默认加载因子:0.75
threshold:扩容的临界值,=容量*填充因子:16 * 0.75 =>12
TREEIFY_THRESHOLD: Bucket中链表长度大于该默认值,转化为红黑树:8MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量:64
HashMap源码解析
jdk7:
调用构造器时,就定默认容量16,默认负载因子0.75(哈希表的装满程度,元素个数/哈希表长度)
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
实际容量使用capacity表示,以2次幂的形式扩增至默认容量
while(capacity < initialCapacity) capacity <<= 1;
用实际容量,负载因子,最大容量计算出吞吐临界值(当前哈希表的最大容量)
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY+1);
根据实际容量造好entry数组。Entry其实是一个接口static class Entry implements Map.Entry{}
table = new Entry[capacity];
计算哈希值并映射到数组索引
int hash = hash(key);
int i = indexFor(hash, table.length);// hash和table.length-1做与运算
添加元素失败,更新相同的key的value。e表示索引位置的元素
if(e.hash == hash && ((k = e.key) == key || key.equals(k)));
V oldValue = e.value;//记录下旧的value值在最后返回
e.value = value;
e.recordAccess(this);
return oldValue;
添加成功,从实参来看,存储key的时候,还把他的hash值也存上了
addEntry(hash,key,value,i);
addEntry方法的具体操作,实际上并不是到了吞吐临界值就马上扩容,而是看是否会形成链表。如果说临界值是12,增加一个元素,要看这个元素是直接装进数组还是形成链表
if((size >= threshold) && (null != table[buckedIndex])){//如果进行了扩容,哈希值要重新计算,相应地映射的索引也要重新计算
resize(2 * table.length);//扩容两倍
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash,key,value,bucketIndex);//到这里才真正地进行添加元素
链表的构造
Entry e = table[bucketIndex];//先取出占位的旧元素
table[bucketIndex] = new Entry(hash,key,value,e);//将新元素放到数组里面
size++;
JDK8:
构造器就不再从一开始就处理默认容量,没有去造数组
this.loadFactor = DEFAULT_LOAD_FACTOR;
jdk8将Entry数组换成了Node数组,定义上没有区别
Node<K,V>[] table;
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
一开始没有创建Node数组,当第一次添加元素先去创建数组
Node<K,V>[] tab; //Node数组的引用,此时还没有造数组
Node<K,V> p; //p表示数组索引位置的旧元素
int n, i;//n表示Node数组的长度,i表示hash值映射到的数组索引
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
如果数组索引位置为空,直接添加新元素
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
如果数组索引位置不空,如果新旧元素哈希值相同且内容相同,则先存下旧元素,然后完成替换
Node<K,V> e; //用于承接和新元素有相同key的链表上的元素
K k;
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
if (e != null) { // existing mapping for key
V oldValue= e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
如果新旧元素哈希值不同,再去和链表上的其他元素比较哈希值。使用两个指针e,p完成链表上的所有元素和新元素的比较,直到将新元素添加到链表末位或者在链表上找到一个和新元素的key相同的元素并完成value的替换
初始阶段p指向数组索引位置的旧元素
Node<K,V> e;
K k;
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {//将新元素添加到链表末位
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st红黑树
treeifyBin(tab, hash);
break;
}
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;//在链表上找到一个和新元素的key相同的元素
p = e;
}
进入上面的红黑树函数,看变成红黑树的条件。并不是链表长度超过8一定变,还要看数组长度是否超过64.因为有时候数组短,链表长,数组一扩容就解决了
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
谈谈你对HashMap中put/get方法的认识?如果了解再谈谈HashMap的扩容机制?默认大小是多少?什么是负载因子(或填充比)?什么是吞吐临界值(或阈值、threshold) ???
LinkedHashMap底层实现-HashMap的子类
可以实现按照添加顺序进行遍历,原理上和HaseSet一样
底层LinkedHashMap重写了put方法中调用的newNode方法
内部类Entry,增加了两个指针指向前驱和后继,然后就是继承自HashMap的构造器
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
Map接口中的常用方法添加、删除、修改操作:
增删改:
object put(Object key, 0bject value): 将指定key-vaLue添加到(或修改)当前map对象中
void putALL (Map m): 将m中的所有key-value对存放到当前map中
object remove(0bject key): 移除指定key的key-vaLue对,并返回vaLue
void cLear():清空当前map 中的所有数据
元素查询的操作:
object get(object key):获取指定key对应的vaLue
boolean containsKey(object key):是否包含指定的key
boolean containsvalue(object value):是否包含指定的vaLue
int size():返回map 中key-value对的个数
boolean isEmpty():判断当前map是否为空
boolean equals(object obj):判断当前map和参数对象obj是否相等
元视图操作的方法:
set keyset():返回所有key构成的Set集合
collection values():返回所有value构成的Collection集合
set entryset():返回所有key-value对构成的Set集合
遍历
Map容器的遍历就没有Iterator了
但是前面讲过了,key用的是Set容器,value用的是Collection容器,我之前一直疑惑这个Collection容器是什么,Map.Entry用的是Set容器
追根溯源,keySet是Set接口的实现类,values是Colleciton接口的实现类,相当于没有使用写好的接口实现类,而是自己定义了实现类
Set s = h.keySet();
Collection c = h.values();
这里的Map.Entry说明Entry是Map的内部接口(还有内部接口???接口内部不是只有全局变量,抽象方法,静态方法,默认方法)
Set entrySet = h.entrySet();
Iterator i = entrySet.iterator();
while(i.hasNext()){
Object obj = i.next();
Map.Entry entry = (Map.Entry)obj;
System.out.println(entry);
}
Map.Entry类还可以调用函数getKey(),getValue()
7. Collections工具类
处理线程不安全的问题
操作set, list,map的工具类
排序(均为静态方法)
reverse(List):反转 List 中元素的顺序
shuffle(List):对 List 集合元素进行随机排序
sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序
sort(List,Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
swap(List,int, int):将指定 list 集合中的 i 处元素和 j 处元素进行交换
查找、替换
Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素
Object min(Collection)
Object min(Collection,Comparator)
int frequency(Collection,Object):返回指定集合中指定元素的出现次数
void copy(List dest,List src):将src中的内容复制到dest中。容易出错,指针越界,需要dest的size>=src.size
List dest = ArrayList.asList(new Object[List.size()]);//将数组转换为顺序表集合
boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换List 对象的所有旧值
四. 泛型
规定容器所能容纳的对象类型
1. 为什么使用泛型
背景:容器的向上向下转型导致的类型丢失。因此采用参数化类型机制,把元素的类型设计为参数,这个类型参数叫做泛型。比如说Collection<E>接口,Collection后面这一坨就叫做类型参数,E属于通配符,是element的简写
类型参数声明在类名之后
之后就将参数化类型扩展到了一般的类,接口,方法
所谓泛型,就是允许在定义类、接口时通过一个标识表示类中某个属性的类型或者是某个方法的返回值的类型。这个标识,也就是类型参数将在使用时(例如,继承或实现这个接口,用这个类型声明变量、创建对象时)确定(即传入实际的类型参数,也称为类型实参)。
泛型的好处是非常直接的,对于类型错误,可以在编译阶段就发现
如果定义类或者接口时声明了类型参数,就说此类或者此接口支持泛型
集合的问题:
(1)类型不安全。无法在编译器层面限制集合元素的类型
(2)因为类型不安全,所以导致在使用集合元素时,如果要求恢复其真实的类型,必须使用向下转型。这种情况下,一旦集合有多种类型的元素,向下转型必然出现类型转换异常
支持泛型之后,相当于底层创建的是泛型数组
泛型的嵌套:Set<Map.Entry<String, Integer>> entry = map.entrySet();
2. 在集合中使用泛型
泛型必须是引用数据类型
支持泛型的类和接口的默认泛型是<Object>
3. 自定义泛型结构
泛型类,接口,方法
泛型不同的引用不能相互赋值。之前static将方法,属性进行了细分,划分为属于类的,以及属于对象的;泛型将类进行了细分。带泛型的类就用 类名泛型 表示
泛型类:
如果定义了泛型类,实例化没有指明类的泛型,则认为此泛型类型为object类型
创建对象时,类型名后面都要加上泛型,构造器也一样。jdk7之后,加上了类型推断,构造器的类名后面只需要写尖括号
异常类不能声明为泛型类
泛型接口:
泛型方法:
泛型方法与 其所在的类或者接口是不是带泛型的 无关,方法带有泛型的结构才叫泛型方法
public List<E> copyArrayToList(E[] array){}
上面这种方法定义还不算泛型方法,编译器会认为E是一种类型
public <E> List<E> copyArrayToList(E[] array){}
泛型方法在调用时,指明泛型参数的具体类型
静态方法不能使用类的泛型。因为静态结构早于对象的创建
类在调用静态方法时,不需要像实例化那样确定泛型参数的类型
class Father<T1>{
T1 a;
public static <E>E show(E x){
return x;
}
}
public class GenericTest {
public static void main(String[] args) {
Son s = new Son();
int[] a = new int[5];
System.out.println(Father.show(a).toString());
泛型方法可以声明为静态
4. 泛型在继承上的体现
子类在继承泛型类时的情况:
我的疑问是:父类中泛型的某个通配符声明了具体的类型,那么在子类中同样的通配符就会被隐去,那子类到底继承的泛型是什么??只继承了父类中未声明具体类型的通配符吗??还是说也继承了声明具体类型的通配符???
我通过实例化对象发现,子类对象要么不继承要么就继承所有的通配符,并且都是未声明具体类型的,无论父类是否声明了具体的类型。都要在实例化子类对象时,确定泛型的具体类型
比如class Son extends Father<Integer>{}
,这种情况下父类的泛型参数指明了具体类型,而子类又不是一个泛型类,他有什么意义呢???????
意义就是继承。比如说
class Father<T1>{
T1 a;
public void show(T1 x){
System.out.println(x);
}
}
class Son extends Father<Integer>{}
public class GenericTest {
public static void main(String[] args) {
Son s = new Son();
s.show(1);
子类继承了父类带泛型参数的方法,同时子类在定义的时候指明了父类泛型参数的类型,那么子类继承的方法的泛型参数就是定义子类时指明的父类泛型参数的类型
但是使用多态的时候又不一样了
Father<Integer> f = new Son();
Father f1 = new Son();
对象f的属性a只能是Integer,而f1则是Object。因为多态规定,父类的引用只能访问到父类的属性和方法
还是没想通!!!!
从继承属性方法的角度看,如果父类是泛型类,且有泛型属性,泛型方法,并且子类不是泛型类,那么子类继承的泛型属性,泛型方法的具体类型是Object类
1.子列也是泛型类的话 子类和父类的泛型类型要保持一致
class ChildGeneric extends Generic
2.子类不是泛型类 父类需要明确泛型的数据类型
class ChildGeneric extends Generic
A:我认为之所以有规定2,是因为继承的成员中有的使用了父类的泛型参数
(1)泛型参数具有子父类关系,但是泛型参数修饰的类是不具备子父类关系。类A是B的父类,G和G二者不具备子父类关系,二者是并列关系
从内存上看,如果同一类型但带有不同泛型参数的引用相等,那就会造成引用规定的类型和实例实际能接受的类型不一致,在编译层面就要禁止这类异常
(2)类A是类B的父类,A
5. 通配符的使用
前面讲到 类A是B的父类,G和G二者不具备子父类关系,二者是并列关系,通配符就用来解决这个问题
List<Object> list1 = new ArrayList<>();
List<String> list2 = new ArrayList<>();
List<?> list = new ArrayList<>();
list = list1;
list = list2;
有时候我们是有这样的需求的,比如定义一个函数,传入一个List容器,然后遍历.可能会设计到多种类型的容器,所以这个地方没有办法用泛型的,泛型都得长一样
public void show(List<Object> list){}
public void show(List<String> list){}
//使用?通配符
public void show(List<?> list){}
?是一个通配符,类A是类B的父类,G和G是没有关系的,二者共同的父类是:G<?>
使用通配符后数据的读取与输入
(1)对于List<?>就不能向其内部添加数据,出了添加null
(2)允许读取数据,读取的数据类型光object
6. 应用
比如DAO(数据访问对象)的创建,DAO用于操作数据库中的表,对每张表进行增删改查。(在java中,每张表都是一个类),这样一来,操作一张表相当于操作一个类。那么对于DAO来说,具体操作哪张表在一开始是不清楚的,那就可以将DAO定义为一个泛型类,在DAO的子类定义确定DAO这个父类的泛型参数的具体类型.DAO的子类不一定是泛型类
public class DAO<T>{}
class CustomerDAO extends DAO<Customer>{}