java基础 —— 高级编程篇

多线程

基本概念

  • 程序:为完成特定任务、用某种语言编写 的一 组指令的集合 。即指 一段静态的代码 ,静态对象。

  • 进程:是程序的一次执行过程,或是 正在运行的一个程序 。是 一个 动态的过程 :有它自身的产生、存在和消亡的 过程 —— 生命周期

    • 如运行中的QQ
    • 进程作为资源分配的最小单位, 系统在运行时会为每个进程分配不同的内存区域
  • 线程 ( thread):进程进一步细化为线程,是一个程序内部的一条执行路径。

    • 若 一个进程同一时间 并行 执行多 个线程,就是支持多 线程的
    • 线程作为调度和执行的最小单位,每个线程拥有独立的运行栈和程序计数器 ( pc),线程切换的开销 小
    • 一个进程中的多个线程共享相同的内存单元 /内存地址空间 ,它们从同一堆中分配 对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来 安全的隐患(同步问题) 。


可类比于原来的单车道(一次只能通过一辆车)变成了现在的多车道(一次可并行通过多辆车)

  • 并行与并发

    • 并行: 多 个 CPU 同时执行多个任务。比如:多个人同时做不同的事 。
    • 并发: 一 个 CPU( 采用时间片 )同时执行多个任务。比如:秒杀、多个人做同一件事。
  • 多线程的优点

    1. 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
    2. 提高计算机系统 CPU 的利用率
    3. 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改
  • 何时需要多线程

    • 程序 需要同时执行两个或多个任务。
    • 程序 需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
    • 需要 一些后台运行的程序时。

线程的创建和使用

Java 语言的 JVM 允许程序运行多个线程,它通过 java.lang.Thread类来体现 。

  • Thread类的特性
    • 每个线程都是通过某个特定 Thread 对象的 run() 方法来完成操作的,经常把 run() 方法的主体称为 线程体
    • 通过该 Thread 对象的 start() 方法启动这个线程,而非直接调用 run() (启动线程后会调用run()方法), 如果自己手动调用 run() 方法,那么就只是普通方法没有启动多线程模式。run 方法由 JVM 调用,什么时候调用,执行的过程控制都有操作系统的 CPU调度 决定 。想要启动多线程,必须调用 start 方法
    • 一 个线程对象只能调用一次 start() 方法启动,如果重复调用了,则将抛出以上的异常“ IllegalThreadStateException“。
Thread类
  • 构造器
1. Thread(): 创建新的 Thread 对象

2. Thread(String threadname): 创建线程并指定线程实例名

3. Thread(Runnable target): 指定创建线程的目标对象,它实现了Runnable 接口中的 run 方法

4. Thread(Runnable target, String name): 创建新的 Thread 对象,实现了Runnable 接口中的 run 方法,并指定线程实例名
  • 方法
void start(): 启动线程,并执行对象的 run() 方法
run(): 线程在被调度时执行的操作
currentThread(): 返回执行当前代码的线程      
String getName(): 返回线程的名称
void setName (String) : 设置该线程名称    
static Thread currentThread (): 返回当前线程 。在Thread子类中就是 this ,通常用于主线程和 									Runnable 实现类           
static void yield() : 线程让步 1. 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
    2. 若队列中没有同优先级的线程,忽略此方法
join() : 当某个程序执行流中调用线程b的 join() 方法时 调用线程将被阻塞,直至线程b执行完为止才开始执			 行调用线程 
static void sleep(long millis) : (指定时间: 毫秒) 1.令当前活动线程在指定时间段内放弃对CPU控制 使其他线程有机会被执行 时间到后重排队。 2. 抛出 InterruptedException 异常
stop(): 强制线程生命期结束,不推荐使用
boolean isAlive(): 返回 boolean ,判断线程是否还活着    
创建(多)线程
  • 继承Thread类
  1. 创建一个继承Thread类的子类

  2. 重写Thread类的run方法 , 将此线程执行的操作声明在run()中

  3. 创建Thread类的子类对象

  4. 通过此对象调用start()

class MyThread extends Thread {
    @Override
    public void run() {
        //执行的操作
    }
}

public class ThreadTest {
    public static void main(String[] args){
        MyThread t = new MyThread();
        
        t.start(); //1. 启动当前线程 2. 调用run()
        
        //也可使用匿名类
        new Thread(){
             @Override
            public void run() {
                //执行的操作
            }
        }.start();
    }
}
  • 实现Runnable接口
  1. 创建一个实现了Runnable接口的类

  2. 实现类实现Runnable中的抽象方法:run()

  3. 创建实现类的对象

  4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象

  5. 通过Thread类的对象调用start()

class MyThread implements Runnable {
    @Override
    public void run() {
        //执行的操作
    }
}

public class ThreadTest {
    public static void main(String[] args){
        MyThread t = new MyThread();
        Thread t1 = new Thread(t);
        t1.start();//调用Thread类中的run(),源码中还是调用了target中的run(),即t中的run(),代理
       }
}

比较创建线程的两种方式

开发中优先选择实现 Runnable 接口的方式

原因: 1. 实现的方式没有类的单继承性的局限性

​ 2. 实现的方式更适合来处理多个线程有共享数据的情况

  • Callable接口

    • 有返回值(支持泛型)、可以抛出异常、需要借助 FutureTask 类
    • Future接口
    1. 可以 对具体 Runnable 、 Callable 任务的执行结果进行取消、查询是否完成、获取结果等。
    2. FutrueTask 是 Futrue 接口的唯一的实现类
    3. FutureTask 同时实现了 Runnable, Future 接口。它既可以作为Runnable 被线程执行,又可以 作为 Future 得到 Callable 的返回值
  • 线程池

    • 背景: 经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大
    • 思路: 提前 创建 好多个线程,放入线程池中,使用时直接 获取,使用完放回池中。可以避免 频繁创建销毁、实现重复利用。类似生活中的公共交通工具。

优点:

  1. 提高响应速度(减少创建新线程的时间)

  2. 降低资源消耗(重复利用线程池中的线程,不需要再次创建)

  3. 便于管理

    ​ CorePoolSize: 核心池的大小

    ​ maximumPoolSize: 最大线程数

    ​ keepAliveTime: 线程没有任务时最多保持多长时间后会终止

class NumberThread implements Runnable{

    @Override
    public void run() {
        //需要执行的操作
        for (int i = 0; i <= 100; i++) {
            if(i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + " : " + i);
            }
        }
    }
}
public class ThreadPool {
    public static void main(String[] args) {
        //创建线程池,容量为:10
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        executorService.execute(new NumberThread());//执行线程工作

        executorService.shutdown(); //关闭线程池

    }
}
ExecutorService :线程池接口。常见子类 ThreadPoolExecutor

void execute(Runnable command) :执行任务 命令,没有返回值,一般用来执行Runnable

<T> Future<T> submit(Callable<T> task) task):执行任务,有返回值,一般执行Callable
    
void shutdown() :关闭连接池

Executors :工具类、线程池的工厂类,用于创建并返回不同类型的线程池

Executors.newCachedThreadPool ()():创建一个可根据需要创建新线程的线程池

Executors.newFixedThreadPool(n ); 创建一个可重用固定线程数的线程池

Executors.newSingleThreadExecutor () :创建一个只有一个线程的线程池

Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
线程的调度
  • 调度策略

    • 时间片轮转

    • 抢占式:高优先级的线程抢占CPU
  • Java 的调度方法

    • 同优先级线程组成先进先出队列(先到先服务),使用时间片策略
    • 对高优先级,使用优先调度的抢占式策略

线程的优先级

MAX_PRIORITY : 10				MIN PRIORITY : 1				NORM_PRIORITY : 5
    
getPriority(): 返回线程优先值    
setPriority(int newPriority): 改变线程的优先级    

线程创建时继承父线程的优先级

低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用

线程的分类

守护线程 和 用户线程

  1. 守护线程是用来服务用户线程的,通过在 start() 方法前调用 thread.setDaemon (true) 可以把一个用户线程变成一个守护线程。
  2. Java 垃圾回收就是一个典型的守护线程。
  3. 若 JVM 中都是守护线程,当前 JVM 将 退出 。
线程的生命周期

五种状态

  • 新建: 当 一个 Thread 类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
  • 就绪: 处于 新建 状态的线程被 start() 后,将进入线程队列等待 CPU 时间片,此时它已具备了运行的条件 ,只是没分配到 CPU 资源
  • 运行: 当就绪的线程被调度并获得 CPU 资源时, 便进入运行状态, run() 方法定义了线程的操作和功
  • 阻塞: 在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
  • 死亡: 线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束

在这里插入图片描述

线程同步

在这里插入图片描述

两个线程同时操作共享数据(互斥资源),导致结果可能被破坏,出现线程安全问题

同步机制

在java中,我们通过同步机制,来解决线程的安全问题

  • 同步锁机制

    对于并发工作,需要某种方式来防止两个任务访问相同的资源 ,其实就是共享资源竞争 。 防止这种冲突的方法就是当资源被一个任务使用时 ,在其上加锁 。 第一个访问某项资源的任务必须锁定这项资源, 使其他任务在其被解锁之前, 就无法访问它了。 而在其被解锁之时, 另一个任务就可以锁定并使用它了。

  • 同步的几种方法

  1. 方式一: 同步代码块
synchronized(同步监视器){ //可以使用当前的 类.class (类也是对象) 
	//操作共享数据的代码 
}

说明:

  1. 操作共享数据的代码,即为需要被同步的代码
    2. 共享数据:多个线程共享(操作)的资源
    3. 同步监视器:即为锁,任何一个对象都可以充当同步锁
    要求:多个线程必须共用同一把锁。(对象必须为共享的,保证唯一即可)
       补充:在实现Runnable接口创建多线程的方式中,可以考率使用this 充当同步监视器
       ​			在继承Thread类创建多线程的方式中,慎用this 充当同步监视器,考虑使用当前类充当

缺点:每次只有一个线程在执行,其他阻塞,效率较低

  1. 方式二:同步方法
public synchronized void show (String name){ //当前类实现Runnable接口
	//这里的同步监视器是 this
}
public static synchronized void show (String name){ //当前类继承Thread类
	静态类的同步监视器是 当前类,类.class
}

同步方法的锁:静态方法(类名.class)、非静态方法(this)

同步代码块的锁:可自己指定,一般使用this或 类名.class

  1. 方式三:Lock锁
ReentrantLock lock = new ReentrantLock();

lock.lock();//上锁,每次只有一个线程访问,显示锁,性能更好
//中间为要锁的代码块
lock.unlock();//解锁

//注意:如果同步代码有异常,要将unlock() 写入 finally 语句块
lock.lock();
try{
	//保证线程安全的代码
}
finally{
	lock.unlock();
}

优先使用顺序

Lock --》同步 代码块(已经进入了方法体,分配了相应资源 )–》同步方法(在方法体之外)

Synchronized 与 Lock的区别

  • synchronized: java提供的内置锁机制,每个JAVA对象可以充当同步锁,线程在访问同步代码块时必须先获取该内置锁,在退出和中断的时候会释放内置锁。Java内置锁通过synchronized关键字使用,使用其修饰方法或者代码块,就能保证方法或者代码块以同步方式执行。(隐式的获取和释放锁)

  • Lock:即显式锁,Lock在使用的时候是显示的获取和释放锁。对于锁的操作具有更强的可操作性、可控制性以及提供可中断操作和超时获取锁等机制, 需要在finally中实现释放锁。

  • Lock是可以中断锁,Synchronized是非中断锁,必须等待线程执行完成释放锁。Lock可以使用读锁提高多线程读效率。

死锁

不同的线程分别占用对方需要的同步资源(不可剥夺资源)不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁,导致都处于阻塞状态,若无外力作用,它们都将无法再向前推进。

产生死锁的必要条件:

  1. 互斥条件:进程要求对所分配的资源进行排它性控制,即一次只有一个进程可使用。如果另一进程申请该资源,那么申请进程应等到该资源释放为止。
  2. 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
  4. 环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。
死锁处理方法

预防死锁

  1. 资源一次性分配:一次性分配所有资源,这样就不会再有请求了(破坏请求条件)

    只要有一个资源得不到分配,也不给这个进程分配其他的资源(破坏请保持条件)

  2. 可剥夺资源:当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)

  3. 资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)

避免死锁

允许进程动态地申请资源。系统在进行资源分配之前预先计算资源分配的安全性。若此次分配不会导致系统进入不安全的状态,则将资源分配给进程;否则,进程等待(不分配资源)。其中最具有代表性的避免死锁算法是银行家算法

死锁检测 + 死锁恢复

由于操作系统有并发,共享以及随机性等特点,通过预防和避免的手段达到排除死锁的目的是很困难的。这需要较大的系统开销,而且不能充分利用资源。为此,一种简便的方法是系统为进程分配资源时,不采取任何限制性措施,但是提供了检测和解脱死锁的手段:能发现死锁并从死锁状态中恢复出来。因此,在实际的操作系统中往往采用死锁的检测与恢复方法来排除死锁。如:

  • 剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态;
  • 撤消进程:可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态消除为止。

线程通信

class Communication implements Runnable {
    int i = 1;

    public void run() {
        while (true) {
            synchronized (this) {
                notify(); //唤醒等待进程, 省略了传入的对象this. 即this.notify()
                if (i <= 100) {
                    System.out.println(Thread.currentThread().getName() + ":" + i++);
                } else
                    break;
                try {
                    wait();//挂起
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
  • wait () 与 notify() 和 notifyAll()
    • wait():令当前线程挂起并放弃 CPU 、 同步资源并等待, 使别的线程可访问并修改共享资源,而当前线程 排队 等候其他线程调用 notify() 或 notifyAll() 方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行。
    • notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待
    • notifyAll ():唤醒正在排队等待资源的所有线程结束等待

注:这三个方法只有在 synchronized 方法或 synchronized 代码块中才能使用,否则会报 java.lang.IllegalMonitorStateException 异常。

因为 这三个方法必须有锁对象调用,而任意对象都可以作为 synchronized 的同步锁,因此这三个方法只能在 Object 类中声明 。

  • 面试题

    1. sleep() 和wait() 的异同
    • 相同点:一旦执行,当前线程进行阻塞状态

    • 不同点:1) 两个方法生命的位置不同,Thread类中声明sleep(),Object类中声明wait()

      ​ 2) 调用的要求不同: sleep() 可以在任何需要的场景下调用。wait() 必须在同步代码块中 使用。

      ​ 3) sleep() 不会释放同步监视器,wait()会释放同步监视器

生产者/消费者问题

问题描述:生产者和消费者在同一时间段内共享一个初始为空、大小为n的缓冲区,生产者负责生产数据存放到缓冲区,而消费者负责消耗缓冲区数据资源。

存在问题:1. 生产者可能会在缓冲区满时存入数据,消费者可能会在缓冲区空时消耗数据。

​ 2. 缓冲区资源为临界资源,每次只允许一个进程进入缓冲区,存在死锁问题

在这里插入图片描述

生产者和消费者对缓冲区互斥访问是互斥关系,同时生产者和消费者又是一个相互协作的关系,只有生产者生产之后,消费者才能消费,他们也是同步关系。

思路: 对生产者,缓存区满则阻塞,消费者从缓存区中取走数据后就唤醒生产者生产。

​ 对消费者,若缓存区为空,则阻塞,生产者生产数据放入缓冲区后就唤醒消费者。

PV操作

信号量mutex作为互斥信号量,用于控制互斥访问缓冲区,互斥信号量初值为 1;

信号量 full 用于记录当前缓冲区中产品数量,初值为0。

信号量 empty 用于记录当前缓冲区还能存放的产品数量,初值为n。

新的数据添加到缓存区中,full + 1,而 empty - 1。如果生产者试图在 empty 为0时减少其值,生产者就会被阻塞。下一轮中有数据被消费掉时,empty + 1,生产者就会被唤醒。

伪代码实现

semaphore mutex=1; //临界区互斥信号量
semaphore empty=n;  //空闲缓冲区,容量为n
semaphore full=0;  //缓冲区初始化为空
producer ()//生产者进程 
{
    while(true)
    {
        生产一个产品;  
        P(empty);  //获取空缓冲区单元  P操作可理解为 -1,V操作为 +1
        P(mutex);  //进入临界区.
        将产品放入缓冲区;  //互斥部分
        V(mutex);  //离开临界区,释放互斥信号量
        V(full);  //缓冲区产品数量加1
    }
}

consumer ()//消费者进程
{
    while(true)
    {
        P(full);  //获取缓冲区单元
        P(mutex);  // 进入临界区
        从缓冲区取出一个产品;  //互斥部分
        V (mutex);  //离开临界区,释放互斥信号量
        V (empty) ;  //空缓冲区数加1
        
    }
}

java代码实现

public class ProducerConsumerTest {
   public static void main(String[] args) {
      CubbyHole c = new CubbyHole();
      Producer p1 = new Producer(c, 1);
      Consumer c1 = new Consumer(c, 1);
      p1.start();
      c1.start();
   }
}
class CubbyHole {
   private int contents;
   private boolean available = false;
   public synchronized int get() {
      while (available == false) {
         try {
            wait();
         }
         catch (InterruptedException e) {
         }
      }
      available = false;
      notifyAll();
      return contents;
   }
   public synchronized void put(int value) {
      while (available == true) {
         try {
            wait();
         }
         catch (InterruptedException e) {
         }
      }
      contents = value;
      available = true;
      notifyAll();
   }
}

class Consumer extends Thread {
   private CubbyHole cubbyhole;
   private int number;
   public Consumer(CubbyHole c, int number) {
      cubbyhole = c;
      this.number = number;
   }
   public void run() {
      int value = 0;
         for (int i = 0; i < 10; i++) {
            value = cubbyhole.get();
            System.out.println("消费者 #" + this.number+ " got: " + value);
         }
    }
}

class Producer extends Thread {
   private CubbyHole cubbyhole;
   private int number;

   public Producer(CubbyHole c, int number) {
      cubbyhole = c;
      this.number = number;
   }

   public void run() {
      for (int i = 0; i < 10; i++) {
         cubbyhole.put(i);
         System.out.println("生产者 #" + this.number + " put: " + i);
         try {
            sleep((int)(Math.random() * 100));
         } catch (InterruptedException e) { }
      }
   }
}

java常用类

String类

  • String 是一个 final 类代表不可变的字符序列, 创建之后不能更改

    String s1 = "abc" // 字面量定义
    String s2 = "abc"
    s1 ="hello" //重新开辟新的内存区域
    

在这里插入图片描述

  • String 对象的字符内容是存储在一个字符 数组 value[] 中 的。(实质是char型数组的封装)
public final class String 
    implements java.io.Serializable, Comparable<String>, CharSequence {
	/** The value is used for character storage. */
	private final char value[];
	/** Cache the hash code for the string */
	private int hash ; // Default to 0
  • String对象的创建

    String str ="hello"
        
    //本质上 this.value = new char[0]
    String s1 = new String();
    
    //this.value = original.value
    String s2 = new String(String original);
    
    //this.value = Arrays.copyOf(value, value.length
    String s3 = new String( char [] a);
    
    String s4 = new String( char [] int startIndex, int count);
    

在这里插入图片描述

在这里插入图片描述

  • String str1= “javaEE”String str2 = new String(“javaEE”) 的区别?
    1. 字符串常量存储在字符串常量池目的是共享
    2. 字符串非常量对象存储在堆中
    3. String str2 = new String(“javaEE”) 在内存中创建了两个对象,一个在堆空间中new结构(地址值),另一个是char[]对应的常量池中的数据:“abc”
String s1 = "javaEE"
String s2 = "javaEE" //存放在字符常量区中
String s3 = new String("javaEE")
String s4 = new String("javaEE") //new + 构造器的方式,保存的是地址值,存放在堆中
     
System.out.println(s1 == s2);//true
System.out.println(s1 == s3);//false
System.out.println(s1 == s4);//false
System.out.println(s3 == s4);//false 

在这里插入图片描述

  • String特性
  1. 常量与常量的拼接结果在常量池 。 且常量池中不会存在相同内容的常量
  2. 只要其中有一个是变量, 结果就在堆中(相当于new的对象)
  3. 如果拼接的结果调用 intern() 方法, 返回值就在常量池中

在这里插入图片描述

String s7 = s4.intern();
System.out.println(s3 == s7) // true
  • 面试题
public class StringTest {
    String str = new String("good");
    char[] ch = { 't', 'e', 's', 't' };
    public void change(String str, char ch[]) {
        str = "test ok";
        ch[0] = 'b';
    }
    public static void main(String[] args) {
        StringTest ex = new StringTest();
        //这里str相当于传递的是形参,不改变原来的值,ch传的是地址值,导致ch的值改变
        ex.change(ex.str, ex.ch);
        System.out.print(ex.str);//good
        System.out.println(ex.ch);//best
    }
}
  
  • String类的转换

    • 字符串 —》基本数据类型、包装类

      parseInt(String s)//其他类型类似
      
    • 包装类 —》字符串

      valueOf(int n)
      
    • 字符数组 —》字符串

      String(char[])//String类构造器
      String(char[],int offset, int length)
      
    • 字符串 —》字符数组

      toCharArray()
      getChars(int srcBegin, int srcEnd, char[] dst)//将指定索引范围字符串存入数组
      
    • 字节数组 —》字符串

      String(byte[])//使用平台默认字符集解码
      String(byte[],int offset, int length)
      
    • 字符串 —》字节数组

      getBytes()//使用平台的默认字符集将此 String 编码为byte 序列,并将结果存储到一个新			的 byte 数组中。
      getBytes(String charsetName) //指定字符集
          
      String str = "中";
      System.out.println(str.getBytes("GBK").length);
      System.out.println(str.getBytes("UTF-8").length);
      
      
  • String 常用方法

    int length() //返回字符串的长度: return value.length
    
    char charAt(int index) //返回某索引处的字符 return value[index]
    
    boolean isEmpty() //判断是否是空字符串: return value.length == 0
    
    String toLowerCase() //使用默认语言环境 将 String 中的所有字符转换为小写
    
    String toUpperCase() //使用默认语言环境 将 String 中的所有字符转换为大写
    
    String trim() //返回字符串的副本 忽略前导空白和尾部空白
    
    boolean equals(Object obj) //比较字符串的内容是否相同
    
    boolean equalsIgnoreCase(String anotherString) //与 equals 方法类似 忽略大小写
    
    String concat(String str) //将指定字符串连接到此字符串的结尾 。 等价于用 "+"
    
    int compareTo(String anotherString) //比较两个字符串的大小
    
    String substring(int beginIndex) //返回一个新的字符串 它是此字符串的从beginIndex 开始截取到最后的一个子字符串 。
    
    String substring(int beginIndex, int endIndex) //返回一个新字符串 它是此字符串从 beginIndex 开始截取到 endIndex( 不包含) 的一个子字符串) 。
        
    boolean contains(CharSequence s) //仅当此字符串包含指定的 char 值序列时,返回 true
    
    int indexOf(String str) //指定子字符串在此字符串中第一次出现处的 索引
    
    int indexOf(String str, int fromIndex) //指定子字符串在此字符串中第一次出现处的索引,从指定的索引 开始
    
    int lastIndexOf(String str) //指定子字符串在此字符串中最右边出现处的 索引
    
    int lastIndexOf(String str, int fromIndex) //指定子字符串在此字符串中最后一次出现处的索引,从指定的索引开始反向 搜索
    //indexOf 和 lastIndexOf 方法如果未找到都是返回-1
        
    String replace(char oldChar, char newChar) // 一个新的字符串 它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的 
    
    String replace(CharSequence target, CharSequence replacement) //用指定的字面值替换序列替换此字符串所有匹配字面值目标序列的子字符串 
    
    String replaceAll(String regex, String replacement) // 用给定的replacement 替换此字符串所有匹配给定的正则表达式的子字符串 
    
    String replaceFirst(String regex, String replacement) //使用 给定的replacement 替换此字符串匹配给定的正则表达式的第一个子字符串 
        
    String[] split(String regex) //根据给定正则表达式的匹配拆分此字符串 
    
    String[] split(String regex, int limit) //根据匹配给定的正则表达式来拆分此字符串,最多不超过 limit 个 ,如果超过了 ,剩下的全部都放到最后一个元素中 
        
    boolean matches(String regex) //告知此字符串是否匹配给定的正则表达式
    

StringBuffer类

  • 可变字符序列,可以对字符串内容进行增删,此时不会产生新的对象。
  • 线程安全,效率低

在这里插入图片描述

  • StringBuffer 类不同于 String ,其对象必须使用构造器生成。有三个构造
  1. StringBuffer():初始 容量为 16 的字符串缓冲区
  2. StringBuffer(int size ):构造指定容量的字符串缓冲区
  3. StringBuffer(String str ):将内容初始化为指定字符串内容
//源码
public StringBuffer(String str) {
        super(str.length() + 16);
        append(str);
 }
StringBuffer s = new StringBuffer('abc') // char[] value = new char["abc".length()+16]
StringBuffer s1 = new StringBuffer()
System.out,println(s1.length()) //0,返回的是count的值,而不是容量 

在这里插入图片描述

  • StringBuffer类的常用方法
StringBuffer append(xxx):用于进行字符串拼接
StringBuffer delete(int start,int end):删除指定位置的内容
StringBuffer replace(int start, int end, String str):把[start,end)位置替换为str
StringBuffer insert(int offset, xxx):在指定位置插入xxx
StringBuffer reverse() :把当前字符序列逆转

append和insert方法可自动扩容

//源码解析
@Override
public StringBUffer append(String s){
	super.append(s);
	return this;
}
public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len);//确保容量足够,不够扩容
        str.getChars(0, len, value, count);
        count += len;
        return this;
}

建议使用StringBuffer(int capacity),指定容量,避免扩容操作导致效率低

由于返回this对象,上述方法都支持方法链操作(obj.method1().method2().method3();

public int indexOf(String str)
public String substring(int start,int end)
public int length()
public char charAt(int n )
public void setCharAt(int n ,char ch)

StringBuilder类

  • 可变字符序列, 线程不安全
  • 与StringBuffer类基本一致

面试题:String、StringBuffer、StringBuilder

  • String:不可变字符序列
  • StringBuffer:可变字符序列、效率低、线程安全
  • StringBuilder:可变字符序列、效率高、线程不安全

注意:作为参数传递的话,方法内部String不会改变其值,StringBuffer和StringBuilder 会改变其值。

java9改进字符串

Java9改进了字符串(包括String、StringBuffer、StringBuilder)的实现。

在Java9以前字符串采用char[]数组来保存字符,因此字符串的每个字符占2字节;

而Java9的字符串采用byte[]数组再加一个encoding-flag字段来保存字符,因此字符串的每个字符只占1字节。所以Java9的字符串更加节省空间,字符串的功能方法也没有受到影响。

日期类

java.lang.System类
//返回当前时间与1970年1月1日0时0分0秒之间以毫秒为单位的时间差
long time = System.currentTimeMillis();
java.util.Date类

---- java.sql.Date类(父子类)

//构造器一:创建当前时间的Date对象
Date date = new Date();
System.out.println(date.toString()); //返回时间, 年、月、日……
System.out.println(date.getTime()); //返回毫秒数

//构造器二:创建指定毫秒数的Date对象
Date date1 = new Date(1550306204104L);

//创建java.sql.Date类
java.sql.Date date2 = new java.sql.Date(1550306204104L);//接收毫秒数
System.out.println(date2); //1972-02-13

//将java.util.Date对象转换为java.sql.Date对象
Date date3 = new Date();
java.sql.Date date4 = new java.sql.Date(date3.getTime());
java.text.SimpleDateFormat类

Date类的API不易于国际化,大部分被废弃了,java.text.SimpleDateFormat 类是一个不与语言环境有关的方式来格式化和解析日期(Date类)的具体类。

在这里插入图片描述

Date date = new Date(); // 产生一个Date实例
// 产生一个formater格式化的实例
SimpleDateFormat formater = new SimpleDateFormat();
System.out.println(formater.format(date));// 打印输出默认的格式 22-9-20 下午10:02
//指定参数为pattern的格式
SimpleDateFormat formater2 = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
System.out.println(formater2.format(date));//2022-09-20 10:03:17
try {
	// 实例化一个指定的格式对象
	Date date2 = formater2.parse("2008-08-08 08:08:08");
	// 将指定的日期解析后格式化按指定的格式输出
	System.out.println(date2.toString());//Fri Aug 08 08:08:08 CST 2008
} catch (ParseException e) {
	e.printStackTrace();
}

DateTomeFormatter类

//类似于SimpleDateFormat,指定格式
DateTomeFormatter dtf = DateTomeFormatter.ofPattern("yyyy-MM-dd");

比较器

实现对象排序的两种方式:

  1. 自然排序:java.lang.Comparable
  • Comparable接口强行对实现它的每个类的对象进行整体排序
  • 实现 Comparable 的类必须实现 compareTo(Object obj) 方法,两个对象即 通过 compareTo(Object obj) 方法的返回值来比较大小。如果当前对象this大于形参对象obj,则返回正整数,如果当前对象this小于形参对象obj,则返回负整数,如果当前对象this等于形参对象obj,则返回零
  • 实现Comparable接口的对象列表(和数组)可以通过 Collections.sortArrays.sort进行自动排序。实现此接口的对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器。
  • Comparable 的典型实现:(默认从小到大排列)
    • String:按照字符串中字符的Unicode值进行比较
    • Character:按照字符的Unicode值来进行比较
    • 数值类型对应的包装类以及BigInteger、BigDecimal:按照它们对应的数值 大小进行比较
    • Boolean:true 对应的包装类实例大于 false 对应的包装类实例
    • Date、Time等:后面的日期时间比前面的日期时间大
//自定义类比较大小要实现Comparable接口,并指明比较大小的方式(重写compareTo方法)
class Goods implements Comparable {
	private String name;
	private double price;
    
	//按照价格,比较商品的大小
    @Override
    public int compareTo(Object o) { 
        if(o instanceof Goods) {
        	Goods other = (Goods) o;
        if (this.price > other.price) {
        	return 1;
        } else if (this.price < other.price) {
        	return -1;
        }
        	return 0;
        }
    	throw new RuntimeException("输入的数据类型不一致");
    }
}


public class ComparableTest{
    public static void main(String[] args) {
        Goods[] all = new Goods[4];
        all[0] = new Goods("《红楼梦》", 100);
        all[1] = new Goods("《西游记》", 80);
        all[2] = new Goods("《三国演义》", 140);
        all[3] = new Goods("《水浒传》", 120);
        Arrays.sort(all);
        System.out.println(Arrays.toString(all));
    }
}

  1. 定制排序:java.util.Comparator
  • 当元素的类型没有实现java.lang.Comparable接口而又不方便修改代码, 或者实现了java.lang.Comparable接口的排序规则不适合当前的操作,那 么可以考虑使用 Comparator 的对象来排序,强行对多个对象进行整体排序的比较。
  • 重写compare(Object o1,Object o2)方法,比较o1和o2的大小:如果方法返 回正整数,则表示o1大于o2;如果返回0,表示相等;返回负整数,表示 o1小于o2。
  • 可以将 Comparator 传递给 sort 方法(如 Collections.sort 或 Arrays.sort), 从而允许在排序顺序上实现精确控制。
  • 还可以使用 Comparator 来控制某些数据结构(如有序 set或有序映射)的 顺序,或者为那些没有自然顺序的对象 collection 提供排序。
Goods[] all = new Goods[4];
all[0] = new Goods("War and Peace", 100);
all[1] = new Goods("Childhood", 80);
all[2] = new Goods("Scarlet and Black", 140);
all[3] = new Goods("Notre Dame de Paris", 120);
Arrays.sort(all, new Comparator() {
    @Override
    public int compare(Object o1, Object o2) {
        Goods g1 = (Goods) o1;  
        Goods g2 = (Goods) o2;
        return g1.getName().compareTo(g2.getName());
    }
});
System.out.println(Arrays.toString(all));

System类

System类代表系统,系统级的很多属性和控制方法都放置在该类的内部。 该类位于java.lang包。

  • 由于该类的构造器是private的,所以无法创建该类的对象,也就是无法实例化该类。其内部的成员变量和成员方法都是static的,所以也可以很方便 的进行调用。

  • 成员变量

    System类内部包含in、out和err三个成员变量,分别代表标准输入流 (键盘输入),标准输出流(显示器)和标准错误输出流(显示器)。

  • 成员方法

    • native long currentTimeMillis(): 该方法的作用是返回当前的计算机时间,时间的表达格式为当前计算机时 间和GMT时间(格林威治时间)1970年1月1号0时0分0秒所差的毫秒数。
    • void exit(int status): 该方法的作用是退出程序。其中status的值为0代表正常退出,非零代表 异常退出。使用该方法可以在图形界面编程中实现程序的退出功能等。
    • void gc(): 该方法的作用是请求系统进行垃圾回收。至于系统是否立刻回收,则取决于系统中垃圾回收算法的实现以及系统执行时的情况。
    • String getProperty(String key): 该方法的作用是获得系统中属性名为key的属性对应的值。系统中常见 的属性名以及属性的作用如下表所示:

在这里插入图片描述

```java
String javaVersion = System.getProperty("java.version");
System.out.println("java的version:" + javaVersion);

String javaHome = System.getProperty("java.home");
System.out.println("java的home:" + javaHome);

String osName = System.getProperty("os.name");
System.out.println("os的name:" + osName);
```

Math类

java.lang.Math提供了一系列静态方法用于科学计算。其方法的参数和返回值类型一般为double型。

abs 绝对值
acos,asin,atan,cos,sin,tan 三角函数
sqrt 平方根
pow(double a,doble b) a的b次幂
log 自然对数
exp e为底指数
max(double a,double b)
min(double a,double b)
random() 返回0.01.0的随机数
long round(double a) double型数据a转换为long型(四舍五入)
toDegrees(double angrad) 弧度—>角度
toRadians(double angdeg) 角度—>弧度

BigInteger与BigDecimal类

BigInteger类
  • Integer类作为int的包装类,能存储的最大整型值为 2 31 − 1 2^{31} - 1 2311,Long类也是有限的, 最大为 2 63 − 1 2^{63}-1 2631

  • java.math包的BigInteger可以表示不可变的任意精度的整数。BigInteger 提供所有 Java 的基本整数操作符的对应物,并提供 java.lang.Math 的所有相关方法。 另外,BigInteger 还提供以下运算:模算术、GCD 计算、质数测试、素数生成、 位操作以及一些其他操作。

  • 构造器

    BigInteger(String val):根据字符串构建BigInteger对象

  • 常用方法

public BigInteger abs():返回此 BigInteger 的绝对值的 BigIntegerBigInteger add(BigInteger val) :返回其值为 (this + val)BigInteger
BigInteger subtract(BigInteger val) :返回其值为 (this - val)BigInteger
BigInteger multiply(BigInteger val) :返回其值为 (this * val)BigInteger
BigInteger divide(BigInteger val) :返回其值为 (this / val)BigInteger。整数相除只保留整										数部分。
BigInteger remainder(BigInteger val) :返回其值为 (this % val)BigIntegerBigInteger[] divideAndRemainder(BigInteger val):返回包含 (this / val) 后跟(this % val) 													的两个 BigInteger 的数组。
BigInteger pow(int exponent) :返回其值为 (thisexponent)BigInteger
BigDecimal类
  • 一般的Float类和Double类可以用来做科学计算或工程计算,但在商业计算中, 要求数字精度比较高,故用到java.math.BigDecimal类。
  • BigDecimal类支持不可变的、任意精度的有符号十进制定点数。
  • 构造器
public BigDecimal(double val)
public BigDecimal(String val)
  • 常用方法
public BigDecimal add(BigDecimal augend)
public BigDecimal subtract(BigDecimal subtrahend)
public BigDecimal multiply(BigDecimal multiplicand)
public BigDecimal divide(BigDecimal divisor, int scale, int roundingMode)

public void testBigInteger() {
    BigInteger bi = new BigInteger("12433241123");
    BigDecimal bd = new BigDecimal("12435.351");
    BigDecimal bd2 = new BigDecimal("11");
   
    //System.out.println(bd.divide(bd2));//这里除不尽则需要指定保留位数,否则报错 
    System.out.println(bd.divide(bd2, BigDecimal.ROUND_HALF_UP));
    System.out.println(bd.divide(bd2, 15, BigDecimal.ROUND_HALF_UP));
}

枚举类

  • 类的对象只有有限个,确定的,可使用枚举类,如星期、性别等

  • 当需要定义一组常量时,强烈建议使用枚举类

  • 枚举类的属性

    1. 枚举 类对象的属性不允许被改动 , 所以应该使用 private final 修饰
    2. 枚举类的使用 private final 修饰的属性应该在构造器中为其赋值
    3. 若枚举类显式的定义了带参数的构造器 , 则在列出枚举值时也必须对应的传入参数
  • 自定义枚举类

  1. 私有化类的构造器,保证不能在类的外部创建其对象
  2. 在类的内部创建枚举类的实例。声明为: public static final
  3. 对象如果有实例变量,应该声明为 private final并在构造器中初始化
class Season{
    private final String SEASONNAME; //季节的名称
    private final String SEASONDESC; //季节的描述
    private Season(String seasonName ,String seasonDesc){
        this.SEASONNAME = seasonName;
        this.SEASONDESC = seasonDesc;
    }
    public static final Season SPRING = new Season( "春天" , "春暖花开");
    public static final Season SUMMER = new Season( "夏天" , "夏日炎炎");
    public static final Season AUTUMN = new Season( "秋天"  "秋高气爽");
    public static final Season WINTER = new Season( "冬天" , "白雪皑皑");
}
  • enum定义枚举类
  1. 使用 enum 定义的枚举类 默认继承 了 java.lang.Enum 类(toString默认打印对象名称),因此不能再继承其他类
  2. 枚举类的构造器只能使用 private 权限修饰符
  3. 枚举类的所有实例必须在枚举类中显式列出 (, 分隔 ; 结尾 )。列出的实例系统会自动添加 public static final 修饰
  4. 必须在枚举类的第一行声明枚举类对象
public enum SeasonEnum{
    
    SPRING( "春天" , "春暖花开"),
 	SUMMER( "夏天" , "夏日炎炎"),
 	AUTUMN( "秋天"  "秋高气爽"),//分隔用逗号
	WINTER( "冬天" , "白雪皑皑");//注意结尾用分号
    
    private final String SEASONNAME; //季节的名称
    private final String SEASONDESC; //季节的描述
    private Season(String seasonName ,String seasonDesc){
        this.SEASONNAME = seasonName;
        this.SEASONDESC = seasonDesc;
    } 
}
  • Enum类常用方法
  1. values() :返回枚举类型的对象数组。该方法可以很方便地遍历所有的枚举值。
  2. valueOf (String str ):可以把一个字符串转为对应的枚举类对象。要求字符串必须是枚举类对象的“名字”。如不是,会有 运行 时异常:IllegalArgumentException 。(根据名称找到枚举类对象)
  3. toString():返回当前枚举类对象常量的名称
SeasonEnum[] values = SeasonEnum.values();
for(int i = 0; i< values.length; i++) {
    System.out.println(values[i]); //SPRING、SUMMER、AUTUMN、WINTER
}
  • 若需每个枚举值在调用实现的接口方法呈现出不同的行为方式 ,则可以让每个枚举值分别来实现该方法

    interface Info() {
        void show();
    }
    
    public enum SeasonEnum implements Info{
        
        SPRING( "春天" , "春暖花开"){
        	@Override
            public void show() {
                //对SPRING具体操作
            }
        },
     	……
    	WINTER( "冬天" , "白雪皑皑"){
        	@Override
            public void show() {
                //对WINTER的具体操作
            }
        };//注意结尾用分号
       
       	……
    }
    

注解Annotation

代码的特殊标记,这些标记可以在编译 , 类加载 , 运行时被读取 , 并执行相应的处理。

框架 = 注解 + 反射 + 设计模式

  • 示例一:生成文档相关的注解
@author	标明开发该类模块的作者 多个作者之间使用 分割
@version	标明该类模块的版本
@see	参考转向 也就是相关主题
@since	从哪个版本开始增加的
@param	对方法中某参数的说明 如果没有参数就不能写
@return	对方法返回值的说明 如果方法的返回值类型是 void 就不能写
@exception	对方法可能抛出的异常进行说明 如果方法没有用 throws 显式抛出的异常就不能写
其中
@param @return@exception 这三个标记都是只用于方法的 。
@param的格式要求: :@param 形参名 形参类型 形参说明
@return的格式要求: :@return 返回值类型 返回值说明
@exception的格式要求: :@exception 异常类型 异常说明
@param@exception 可以并列多个

    
package com.annotation.javadoc;
/**
*@author shkstart
*@version 1.0
*@see Math.java
*/
public class JavadocTest {
    /**
    *程序的主方法,程序的入口
    *@param args String[] 命令行参数
    */
    public static void main(String[] args )
    }
    /**
    *求圆面积的方法
    *@param radius double 半径值
    *@return double 圆的面积
    */
    public static double getArea( double radius){
        return Math. PI * radius * radius
    }
}
  • 示例二: 在编译时进行格式 检查 (JDK 内置的三个基本注解)

    • @Override: 限定重写父类方法 , 该注解只能 用于方法
    • @Deprecated : 用于表示所修饰的元素类 , 方法等已过时。通常是因为所修饰的结构危险或存在更好的选择
    • @SuppressWarnings : 抑制编译器警告
  • 自定义注解

    • 注解声明: @interface
    • Annotation 的成员变量在 Annotation 定义中以无参数方法的形式来 声明 。 类型只能是八种基本数据类型 、 String 类型 、 Class 类型 、 enum 类型 、 Annotation 类型 、以上所有类型的数组。
    • 可以在定义 Annotation 的成员变量时为其指定初始值 指定成员变量的初始值可使用 default 关键字
    • 如果定义的注解含有配置参数 那么使用时必须指定参数值 除非它有默认值 。 格式是 参数名 参数值 如果只有一个参数成员 且名称为 value可以省略 ”value=“
    • 没有成员定义的 Annotation 称为 标记 ;包含成员变量的 Annotation 称为元数据 Annotation

注意:自定义注解必须配上注解的信息处理流程才有意义 。

public @interface MyAnnotation {
    //注解参数:参数类型 + 参数名();
    String value() default "hello"; //一个成员通常使用value表示 ,这里不传参默认为hello
}

@MyAnnotation(value="world")
class Person {
    //……
}
  • JDK中四个元注解(注解的注解)
  • @Retention : 只能用于修饰一个 Annotation 定义 , 用于指定该 Annotation 的生命周期 , @Rentention 包含一个 RetentionPolicy 类型的成员变量 , 使用Rentention 时必须为该 value 成员变量指定值:
    • RetentionPolicy.SOURCE 在源文件中有效(即源文件保留),编译器 直接丢弃这种策略的注释
    • RetentionPolicy.CLASS 在 class 文件中有效(即 class 保留) 当运行 Java 程序时 , JVM不会保留注解。 这是默认值
    • RetentionPolicy.RUNTIME 在运行时有效(即运行时保留), 当运行 Java 程序时 , JVM 会
      保留注释。程序 可以通过反射获取 该注释。

在这里插入图片描述

public enum RetentionPolicy
    SOURCE,
    CLASS,
    RUNTIME;
}

@Retention(RetentionPolicy.SOURCE)
@interface MyAnnotation1{ }

@Retention(RetentionPolicy.RUNTIME)
@interface MyAnnotation2{ }
  • @Target : 用于修饰 Annotation 定义 , 用于指定被修饰的 Annotation 能用于修饰哪些程序元素。 @Target 也包含一个名为 value 的 成员变量。

在这里插入图片描述

  • @Documented: 用于指定被该元 Annotation 修饰的 Annotation 类将被javadoc 工具提取成文档。 默认情况 下 javadoc 是不包括注解的 。

    • 定义为 Documented 的注解必须设置 Retention 值为 RUNTIME
  • @Inherited: 被它修饰的 Annotation 将具有继承性 。如果 某个类使用了被@Inherited 修饰的 Annotation, 则其子类将自动具有该注解。比如:如果把标有 @Inherited 注解的自定义的注解标注在类级别上,子类则可以继承父类类级别的注解

@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
    String[] value();
}
  • 利用反射获取注解信息
    在这里插入图片描述

  • 可重复注解
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

  • 类型注解

    JDK1.8 之后,关于元注解 @Target 的参数类型 ElementType 枚举值多了两个:TYPE_PARAMETER,TYPE_USE

    • ElementType.TYPE_PARAMETER 表示该注解能写在类型变量的声明语
      句中 如 泛型声明 。
    • ElementType.TYPE_USE 表示该注解能写在使用类型的任何语句中

集合

概述

  • 数组在存储数据方面的弊端

    • 数组初始化以后,长度就不可变了,不便于扩展,且初始化后就决定了数组存储的类型
    • 数组中提供的属性和方法少,不便于进行添加、删除、插入等操作, 且效率不 高。同时无法直接获取存储元素的个数。
    • 数组存储的数据是有序的、可以重复的。 存储数据的特点单一
  • Java 集合就像一种容器,可以动态地 把多个对象的引用放入容器中。

  • Java集合可分为 CollectionMap 两种体系

    • Collection 接口:单列数据, 定义了存取一组对象的方法的集合
      • List :元素 有序、可 重复的集合
      • Set :元素无序、不可重复的集合
    • Map 接口: 双列数据,保存具有映射关系“ key-value 对”的集合

在这里插入图片描述
在这里插入图片描述

Collection接口

  • Collection 接口是 List 、 Set 和 Queue 接口的父接口,该接口里定义的方法既可用于操作 Set 集合,也可用于操作 List 和 Queue 集合 。

    //添加
    add(Object obj)
    addAll(Collection coll)
    //获取有效元素的个数
    int size()    
    //清空集合    
    void clear()     
    //是否是空集合
    boolean isEmpty()  
    //是否包含某个元素
    boolean contains(Object obj): 是通过元素的 equals 方法来判断是否是同一个对象    
    boolean containsAll(Collection c): 调用元素的equals方法来比较。拿两个集合的元素挨个比较 。   
    //删除
    boolean remove(Object obj) 通过 元素的 equals 方法判断是否是
    //要删除的那个元素。只会删除找到的第一个元素
    boolean removeAll(Collection coll 取当前集合的差集
    //取两个集合的交集
    boolean retainAll(Collection c) 把交集的结果存在当前集合中,不影响c
    //集合是否相等
    boolean equals(Object obj)
    //转成对象数组
    Object[] toArray()
    //获取集合对象的哈希值
    hashCode()
    //遍历
    iterator() :返回迭代器对象,用于集合遍历    
    

Iterator 迭代器接口

**Iterator 对象称为迭代器 (设计模式的一种 ),主要用于遍历 Collection 集合中的元素。**提供一种方法访问一个容器 对象中各个元素,而又不需暴露该对象的内部细节。
在这里插入图片描述
在这里插入图片描述

在调用it.next 方法之前必须要调用 it.hasNext 进行检测。若不调用,且下一条记录无效,直接调用 it.next 会抛出 NoSuchElementException 异常。

Iterator iterator = coll.iterator();
//hasNext():判断是否还有下一个元素
while(iterator.hasNext()){
//next(): ① 指针下移 ②将下移以后集合位置上的元素返回
    System.out.println(iterator.next());
}

集合对象每次调用 iterator() 方法都得到一个全新的迭代器对象 ,默认游标在集合的第一个元素之前

List接口

  • List 集合类中 元素有序、且可重复 ,集合中的每个元素都有其对应的顺序索引。
  • List 接口的实现类常用的有: ArrayList 、 LinkedList 和 Vector
    • ArrayList: List接口的主要实现类;线程不安全,效率高;底层使用Object[] elementData存储
    • LinkedList: 对于频繁的插入、删除操作,使用此类效率比ArrayList高;底层使用双向链表存储
    • Vector: List接口的古老实现类;线程安全,效率低;底层使用Object[] elementData存储
ArrayList源码分析
//构造器
public ArrayList() {
    //底层Object[] elementData,初始化为{},没有初始化长度
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

//add方法,第一次调add方法才创建长度为10的数组,懒汉式
public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
}

private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    	//第一次调add方法,确定长度,10
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code 第一次调elementData长度是0
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
}
private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1); //扩容1.5倍
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);//将原有数组数据复制到新数组
}


ArrayList list = new ArrayList();
list.add("123");

建议使用带参构造器:ArrayList list = new ArrayList(int capactity);

LinkedList源码分析
//双向链表
int size = 0;
Node<E> first; //链表表头
Node<E> last;//链表表尾

private static class Node<E> {
       E item;
       Node<E> next;//记录下一个元素的位置
       Node<E> prev;//记录前一个元素的位置

       Node(Node<E> prev, E element, Node<E> next) {
           this.item = element;
           this.next = next;
           this.prev = prev;
       }
}

public boolean add(E e) {
       linkLast(e);
       return true;
}
void linkLast(E e) {
       final Node<E> l = last;
       final Node<E> newNode = new Node<>(l, e, null);
       last = newNode;
       if (l == null)
           first = newNode;
       else
           l.next = newNode;
       size++;
       modCount++;
}
//Node定义
private static class Node<E> {
       E item;
       Node<E> next;
       Node<E> prev;

       Node(Node<E> prev, E element, Node<E> next) {
           this.item = element;
           this.next = next;
           this.prev = prev;
       }
}

LinkedList linkedList = new LinkedList(); // 默认为null
linkedList.add("12");

在这里插入图片描述

方法
void add( int index, Object ele): 在 index 位置插入 ele 元素

boolean addAll(int index, Collection eles): 从 index 位置开始将 eles中的所有元素添加进来

Object get( int index): 获取指定 index 位置的元素

int indexOf (Object obj): 返回 obj 在集合中首次出现的位置

int lastIndexOf (Object obj): 返回 obj 在当前集合中末次出现的位置

Object remove(int index): 移除指定 index 位置的元素,并返回此元素

Object set(int index, Object ele: 设置指定 index 位置的元素为 ele

List subList(int fromIndex , int toIndex): 返回从 fromIndex 到 toIndex 位置的子集合
面试题
@Test
public void testListRemove() {
    List list = new ArrayList();
    list.add(1);
    list.add(2);
    list.add(3);
    updateList(list);
    System.out.print(list); 
}
private static void updateList(List list ){
	list.remove(2); //默认是指index,需要移除2时可以用list.remove(new Integer(2));
}
  • 请问ArrayList/LinkedList/Vector 的 异同 谈谈你的理解? ArrayList 底层是什么?扩容机制? Vector 和 ArrayList 的最大区别
  1. ArrayList 和 LinkedList 的 异同

​ 二者都线程不安全,相对线程安全的Vector ,执行效率高。此外,ArrayList 是实现了基于动态数组的数 据结构, LinkedList 基于链表的数据结构。对于随机访问 get 和 set ArrayList 觉得优于 LinkedList , 因为LinkedList 要移动指针。对于新增和删除 操作 add( 特指 插入 和 remove LinkedList 比较占优势,因 为ArrayList 要移动数据。

  1. ArrayList 和 Vector 的区别
    Vector和 ArrayList 几乎是完全相同的 唯一的区别在于 Vector 是同步类 ( synchronized),属于强同步类。因此开销就比 ArrayList 要大,访问要慢。正常情况下 大多数的 Java 程序员使用ArrayList 而不是 Vector, 因为同步完全可以由程序员自己来控制。 Vector 每次扩容请求其大小的 2 倍空间,而 ArrayList 是 1.5 倍。 Vector 还有一个子 类 Stack 。

Set接口

  • set 接口没有提供额外的方法
  • Set 集合不允许包含相同的元素,如果试把两个相同的元素加入同一个Set 集合中,则添加操作失败。
  • Set 判断两个对象是否相同不是使用 == 运算符,而是 根据 equals() 方法
HashSet
  • HashSet 按 Hash 算法来存储集合中的元素,因此具有很好的存取、查找、删除性能。
  • 特点
    • 无序(不等于随机性, 以某种散列排序)、不可重复
    • HashSet 线程不安全
    • 集合元素可以是 null
  • HashSet 集合判断两个元素相等的标准: 两个对象通过 hashCode () 方法比较相等,并且两个对象的 equals() 方法返回值也相等 。
  • 对于存放在 Set 容器中的对象, 对应的类一定要重写 equals 和 hashCode(Objectobj) 方法,以实现对象相等规则 。即: :“相等的对象必须具有相等的散列码
  • hashSet 底层实现是HashMap,存储的值以key值存入map中,value值为常量
  • 向 HashSet 中添加元素的过程
  1. 当向 HashSet 集合中存入一个元素时 ,HashSet 会调用该对象的 hashCode() 方法来得到该对象的 hashCode 值 ,然后根据 hashCode 值 ,通过某种散列函数决定该对象在 HashSet 底层数组中的存储位置 。 这个散列函数会与底层数组的长度相计算得到在数组中的下标, 并且这种散列函数计算还尽可能保证能均匀存储元素 ,越是散列分布该散列函数设计的越好
  2. 若两个元素的 hashCode() 值相等, 会继续调用 equals 方法 ,如果 equals 方法结果为 true, 添加 失败。 如果 为 false, 那么会保存该元素 但是该数组的位置已经有元素了那么会通过 链表的方式继续链接
  • 如果 两个元素的 equals() 方法返回 true ,但它们的 hashCode () 返回值不相等, hashSet 将会把它们存储在不同的位置,但依然可以添加成功 。
  • 底层:数组+链表

在这里插入图片描述

底层也是数组初始容量为 16 当如果使用率超过 0.75, (16 * 0.75 =12)就会扩大容量为原来的 2 倍 。 (16 扩容为 32, 依次为 64,128 等)

源码分析

/*重写hashCode时
1. 选择系数的时候要选择尽量大的系数(扩大原有数的差距)。因为如果计算出来的 hash 地址越大,所谓的“冲突”就越少,查找起来效率也会提高。(减少冲突)
2.并且 31 只占用 5bits, 相乘造成数据溢出的概率较小。
3.31可以 由 i*31== (i<<5)- 1 来表示, 现在很多虚拟机里面都有做相关优化。(提高算法效率)
4.31是一个素数,素数作用就是如果我用一个数字来乘以这个素数,那么最终出来的结
果只能被素数本身和被乘数还有1来整除。(减少冲突)
*/
@Override
public int hashCode() {
    int result = name != null ? name.hashCode() : 0;
    result = 31 * result + id;
    result = 31 * result + age;
    return result;
}

散列表(hash table)

散列表是根据关键码值(Key value)进行访问的数据结构。通过把关键码值按照预定规则映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表

例:将0-10的数存储到一个数组中,对应下标也刚好为0-10. 则对应映射 key = f(key), 该函数即为简单的散列函数,现重新设计更复杂的函数,使得key值在散列表中分布更均匀。

散列函数

**这个f()即散列函数 (哈希函数)。散列函数有多种,如:直接定址法、数字分析法、平方取中法、取余法、随机数法等。

构造方法

  1. 直接定址法

    取关键字或关键字的某个线性函数值为哈希地址:H(key) = key 或 H(key) = a*key + b

  2. 平方取中法

    取关键字平方后的中间几位为哈希地址。通过平方扩大差别,另外中间几位与乘数的每一位相关,由此产生的散列地址较为均匀。

  3. 除留余数法

    取关键字被数p除后所得余数为哈希地址:H(key) = key mod p (p ≤ m)。

    注意:这是一种最简单,也最常用的构造哈希函数的方法。它不仅可以对关键字直接取模(MOD),也可在折迭、平方取中等运算之后取模。值得注意的是,在使用除留余数法时,对p的选择很重要。一般情况下可以选p为小于表长的最大质数。

哈希冲突

​ 当不同key值通过散列函数映射到相同位置时,就会产生哈希冲突。

解决方案:

散列冲突的解决方案分为两种,① 开放地址法和 ② 链地址法

开放地址法又分为三种,线性探测法、平方探测法、以及再哈希法。

  1. 线性探测法

    将产生冲突的数据存入紧接着的下一个位置

    121921

    当前hash表中存储如上,当有一个key=9的数要存入19所在的位置,但该位置已经存了数据,产生冲突,则向后存。下一位置存储了21,再次冲突。继续向后找,知道找到不冲突的位置,即21后面的位置存入9

    存在问题:线性探测法会导致大量的数据聚集在那一片区域,导致解决冲突的效率并不高

  2. 平方探测法

    上边的线性探测法是将下标索引向后找,步长为1;平方探测法是将下标索引向后找,步长为 1 2 1^2 12 2 2 2^2 22等(认为设定)。

  3. 再散列法

    事先准备多个散列函数(一般最多三个就可以解决大部分的问题)

    当将要存入的这个数据与已有的数据产生了哈希冲突时,此时将该数据通过第二个哈希函数进行哈希计算,按照第二个哈希函数计算出来的位置重新存入数据,如果还是冲突,再继续通过第三个哈希函数进行计算…

链地址法

把具有相同散列地址的记录放在同一个单链表中,称为同义词链表。有 m 个散列地址就有 m 个单链表,同时用数组Head[0…m-1]存放各个链表的头指针,凡是散列地址为i的记录都以结点方式插入到以 Head[i]为头结点的单链表中。

在这里插入图片描述

        不管是线性探测法还是平方探测法,都会导致数据产生一系列的偏移,不利于后期的查找。而链地址法的话,数据都还是在数组中的同一个位置,这样的话更符合散列函数的定义。

应用:SHA-1和MD5算法

如何评价设计的散列函数

  • 较低的填装因子

装填因子 = 散列表包含的元素个数 数组长度 装填因子 = \frac{散列表包含的元素个数} {数组长度} 装填因子=数组长度散列表包含的元素个数

1235

装填因子 = 4 5 装填因子 = \frac{4} {5} 装填因子=54

填装因子大于1意味着元素的数量超过了数组的长度,此时再继续存储元素就一定会发生哈希冲突;

填装因子越低,发生冲突的可能性就越小,散列表的性能即越高

一旦填装因子大于0.7,就调整散列表的长度

如何调整散列表的长度呢:

  1. 首先,创建一个更长的数组,通常将数组增长一倍;
  2. 接下来,使用hash函数将所有的元素都插入到这个新的散列表中。

调整散列表长度的工作的开销很大,需要很长时间!因此尽量避免频繁调整散列表的长度的操作。但平均而言,即便考虑到调整长度所需的时间,散列表操作所需的时间也为O(1)。

LinkedHashSet
  • LinkedHashSet 是 HashSet 的子类
  • LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,但它同时使用 双向链表 维护元素的次序,这使得元素看起来是以插入顺序保存 的。
  • LinkedHashSet 插入性能略低于 HashSet ,但在迭代访问 Set 里的全部元素时有很好的性能。

源码解析

/**
* Constructs a new, empty linked hash set with the default initial
* capacity (16) and load factor (0.75).即默认容量 0.75 * 16 = 12
*/
public LinkedHashSet() {
        super(16, .75f, true);
}

Set set = new LinkedHashSet();
set.add(234);
TreeSet
  • TreeSet 是 SortedSet 接口的实现类, TreeSet 可以确保集合元素处于排序状态
  • TreeSet 底层使用红黑树结构存储数据
  • TreeSet 两种排序方法: 自然排序定制排序 。默认情况下, TreeSet 采用自然排序。
  • TreeSet要求添加的数据为相同类型的结构
  • 特点:有序查询速度比 List 快

自然排序

  • TreeSet 会调用集合元素的 compareTo (Object obj ) 方法来比较元素之间的大小关系,然后将集合元素 按 升序 默认情况 排列
  • 如果试图把一个对象添加到 TreeSet 时,则该对象的类必须实现 Comparable接口(告诉怎么排)
  • 向 TreeSet 中添加元素时,只有第一个元素无须比较 compareTo 方法,后面添加的所有元素都会调用 compareTo 方法进行比较。
public TreeSet() {
   this(new TreeMap<E,Object>());
}

定制排序

  • 通过Comparator 接口来实现。 需要重写 compare(T o1,T o2) 方法
  • 要 实现定制排序,需要将实现 Comparator 接口的实例作为形参传递给 TreeSet 的构造器。
public TreeSet(Comparator<? super E> comparator) {
    this(new TreeMap<>(comparator));
}
  • 面试题
HashSet set = new HashSet();
Person p1 = new Person(1001,"AA");
Person p2 = new Person(1002,"BB");
set.add(p1);
set.add(p2);
// 修改了原有对象的name值,但还是根据原来{"AA",1001}的hashCode值存入set的位置,位置没变
p1.name = "CC";
//当需要移除p1shi时,根据{"CC",1001}对应的hashCode值找不到p1,所以没有删掉
set.remove(p1);
System.out.println(set); // 简写:CC,BB; 
set.add(new Person(1001,"CC"));
System.out.println(set);
//hashCode值相同时,会比较equal方法,发现不一样后会存入set中
set.add(new Person(1001,"AA"));
System.out.println(set);

Map接口

  • Map :用于保存具有 映射关系 的 数据 :key-value

    • HashMap: Map的主要实现类,线程不安全,效率高,可存储null的key和value

      • LinkedHashMap:保证添加顺序, 在原有HashMap底层结构上,添加了前后指针
    • TreeMap:实现按key值排序遍历,底层红黑树

    • Hashtable:古老实现类;线程安全,效率低

      • Properties: 常用于处理配置文件;key和value都为String类型
  • Map 中的 key 用 Set 来存放无序不允许重复 ,即同一个 Map 对象所对应的类,须重写 hashCode 和 equals 方法

  • 所有的 value 构成的集合是 Collection: 无序的、 可以重复 的。所以, value 所在的类要重写: equals()

  • 一 个 key-value 构成一 个 Entry对象(条目),使用Set存储所有entry
    在这里插入图片描述

常用方法

//添加 、 删除、修改操作
Object put(Object key,Object value) value):将指定key-value添加到或修改当前map对象中
void putAll(Map m): 将m中的所有key-value对存放到当前map中
Object remove(Object 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集合
public static void main(String[] args) {
        Map map = new HashMap();
        //map.put(..,..)省略
        System.out.println("map 的所有 key:");
        Set keys = map.keySet();// HashSet
        for(Object key : keys ){
            System.out.println(key + "-->"+ map.get(key));
        }
        System.out . println("map 的所有的 value");
        Collection values = map.values();
        Iterator iter = values.iterator();
        while(iter.hasNext()){
            System.out.println(iter.next());
        }
        System.out.println("map 所有的映射关系:");
        //映射关系的类型是Map.Entry类型,它是Map接口的内部接口,为了更方便地输出map键值对
        Set mappings = map.entrySet();
        for(Object mapping : mappings ){
            Map.Entry entry = (Map.Entry) mapping;
            System.out.println ("key是:"+ entry.getKey()
                                + "value是:" + entry.getValue());
        }
}
HashMap
  • 允许使用 null 键和 null 值
  • 线程不安全,效率高
  • 所有 的 key 构成的集合是 Set: 无序的、不可重复 的。所以, key 所在的类要重写:equals() 和 hashCode()
  • 所有 的 value 构成的集合是 Collection: 无序的、可以重复 的。所以, value 所在的类
    要重写: equals()
  • 所有的 entry 构成的集合是 Set: 无序的、不可重复的
  • HashMap 判断两个 key 相等的标准 是:两个 key 通过 equals() 方法返回 true,hashCode 值也相等。
  • HashMap 判断两个 value 相等的标准 是:两个 value 通过 equals() 方法返回 true
  • JDK8中,HashMap 是数组+链表+红黑树实现

在这里插入图片描述
在这里插入图片描述

JDK8底层实现原理分析

HashMap map = new HashMap();
  1. 实例化后, 底层没有创建一个长度为16的数组, 而是在调用put()方法时创建的(饿汉式
  2. 底层数组为:Node[],而非JDK7中的Entry[]
  3. JDK8中地层结构为:数组+链表+红黑树;当数组的某一索引位置上的元素以链表形式存在的数据个数 >= 8, 且当前数组长度 > 64时,此时此索引位置上的所有数据改为使用红黑树存储。

map.put(key1, value1)过程:

首先,调用key1所在类的hashCode()计算 key1 的哈希值, 此哈希值经过计算后, 得到在数组中要存储的位置 i

如果 位置 i 上没有元素, 则添加成功

如果 位置 i 上已经存在一个或多个元素 ,则需要通过循环的方法 ,依次比较key1 和 其他数据的哈希值。如果彼此哈希值不同,则直接添加成功 。 如果哈希值与某个数据(key2-vaule2)的哈希值相同,继续比较,调用key1所在类的equals方法

如果 返回值为 true ,则使用 value1去替换 value2

如果遍历一遍以后发现所有的 equals 返回都为 false, 则添加成功

在不断添加的过程中,存在扩容问题,默认扩容方式:扩容为原来的两倍,并将原来的数据复制过去(重新计算哈希值)。

//源码
transient Node<K,V>[] table;
//以Node结构存储
static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
}

//put方法
 public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
}

//获取哈希值
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}  

/**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
    	//最开始没有初始化,第一次调用后初始化
        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);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                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;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
}

/**
     * Initializes or doubles table size.  If null, allocates in
     * accord with initial capacity target held in field threshold.
     * Otherwise, because we are using power-of-two expansion, the
     * elements from each bin must either stay at same index, or move
     * with a power of two offset in the new table.
     *
     * @return the table
*/
final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];// 这里才new的数组
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
}

/**
     * Replaces all linked nodes in bin at index for given hash unless
     * table is too small, in which case resizes instead.
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
    	//这里比较了数组的长度,如果数组长度 < 64 ,则扩容,不变成树。这是考虑到设计的散列函数问题,导致存在较严重的哈希冲突,数据比较集中。
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
}

重要常量

DEFAULT_INITIAL_CAPACITY : HashMap 的默认容量16

DEFAULT_LOAD_FACTOR: HashMap 的默认加载因子0.75f

TREEIFY_THRESHOLD: Bucket 中链表长度大于该默认值,转化为红黑树, 8

threshold: 扩容的临界值 = 容量 *填充因子 16 * 0.75 = 12

MIN_TREEIFY_CAPACITY:桶中的 Node 被树化时最小的 hash 表容量,64

面试题

负载因子值的大小,对 HashMap 有什么影响

  • 负载 因子的大小决定了 HashMap 的数据密度
  • 负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长,造成查询或插入时的比较次数增多,性能会下降。
  • 负载因子越小,就越容易触发扩容,数据密度也越小,意味着发生碰撞的几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性能会更高。但是会浪费一定的内容空间。而且经常扩容也会影响性能,建议初始化预设大一点的空间。
  • 按照其他语言的参考及研究经验,会考虑将负载因子设置为 0.7~0.75 ,此时平均检索长度接近于常数。

HashMap的扩容机制

  • 当元素数量超过阈值(16 * 0.75)时便会触发扩容,每次扩容的容量都是之前容量的2倍。
  • HashMap的容量是有上限的,必须小于1<<30,即1073741824。如果容量超出了这个数,则不再增长,且阈值会被设置为Integer.MAX_VALUE 2 31 − 1 2^{31}−1 2311 ,即永远不会超出阈值了)。
  • 存放新值时, 如果某个位置链表的元素个数 > 8 ,但数组长度 < 64 ,也会触发扩容(是否生成红黑树)
LinkedHashMap
  • LinkedHashMap 是 HashMap 的子类,内部类是Entry(HashMap内部类是Node)
  • 在 HashMap 存储结构的基础上,使用了一对双向链表来记录添加元素的顺序,迭代顺序与 Key - Value 对的插入顺序一致
//源码

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);
        }
}

/**
* The head (eldest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> head;

/**
* The tail (youngest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> tail;

//重写了HashMap中的方法
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
        LinkedHashMap.Entry<K,V> p =
            new LinkedHashMap.Entry<K,V>(hash, key, value, e);
        linkNodeLast(p);
        return p;
}
TreeMap
  • TreeMap 存储 Key-Value 对时, 需根据 key -value 对进行排序,保证所有 Key- Value 对处于有序状态
  • TreeSet 底层使用 红黑树 结构存储 数据
  • TreeMap 的 Key 的排序:
    • 自然排序: TreeMap 的所有的 Key 必须实现 Comparable 接口,而且所有的 Key 应该是同一个类的对象,否则将会抛出 ClasssCastException
    • 定制排序 :创建 TreeMap 时,传入一个 Comparator 对象,该对象负责对TreeMap 中的所有 key 进行排序。此时不需要 Map 的 Key 实现Comparable 接口
    • TreeMap 判断两个 key 相等的标准 :两个 key 通过 compareTo() 方法或者 compare() 方法返回 0
Properties
  • Properties 类是 Hashtable 的子类,该对象用于处理属性文件
  • 由于属性文件里的 key 、 value 都是字符串类型,所以 Properties 里的 key和 value 都是字符串类型
  • 存取数据时,建议使用 setProperty (String key,String value) 方法和getProperty (String) 方法
Properties pros = new Properties();
pros.load(new FileInputStream("jdbc.properties");
String user = pros.getProperty("user");
System.out.println(user); 

Collections工具类

Collections 是一个操作 Set 、 List 和 Map 等集合的工具类,中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法

  • 排序操作
reverse(List) 反转 List 中元素的顺序
shuffle(List)List 集合元素进行随机排序,即打乱
sort(List) 根据元素的自然顺序对指定 List 集合元素按升序排序
sort(ListComparator) 根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
swap(Listint, 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 中
boolean replaceAll (List list, Object oldVal, Object newVal) 使用新值替换List 对象的所有旧值
  • 同步控制

Collections 类中提供了多个 synchronizedXxx () 方法,该方法可使将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题


泛型

Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是==参数化类型==,也就是说所操作的数据类型被指定为一个参数

  • 泛型的设计背景

​ 集合容器类在设计阶段/声明阶段不能确定这个容器到底实际存的是什么类型的对象, 所以在 JDK 1.5 之前只能把元素类型设计为 Object, JDK 1.5 之后使用泛型来解决 。 因为这个时候除了元素的类型不确定, 其他的部分是确定的. 例如关于这个元素如何保存 ,如何管理等是确定的 。因此此时把元素的类型设计成一个参数, 这个类型参数叫做泛型 。 Collection, List ,ArrayList 这个 就是类型参数 ,即泛型 。

  • 所谓 泛型, 就是允许在定义类 、 接口时通过一个标识表示类中某个属性的类型或者是某个方法的返回值及参数类型 。 这个类型参数将在使用时(例如继承或实现这个接口 、用这个类型声明变量 、 创建对象时) 确定(即传入实际的类型参数 ,也称为类型实参 )

为什么要引入泛型

  1. 解决元素存储的安全性问题 好比商品 、 药品标签 不会弄错 。
  2. 解决获取数据元素时 需要类型强制转换的问题 好比不用每回拿商品 、 药品都要辨别

在集合中没有泛型时

在这里插入图片描述

在集合中有泛型时

在这里插入图片描述

Java泛型可以保证如果程序在编译时没有发出警告,运行时就不会产生ClassCastException 异常。同时,代码更加简洁、健壮。

泛型的使用

ArrayList<Integer> list = new ArrayList 类型推断
list.add(78);
list.add(79);
list.add(76);
list.add("asdas")//报错,因为指定了为Integer类型
//遍历方式一:
//for(Integer i : list){
	//不需要强转
	//System.out.println(i);
//}
//遍历方式二:
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()){
    System.out.println(iterator.next());
}

Map<String,Integer> map = new HashMap<String,Integer>();

注:1. 例:add(E e) —》 实例化后:add(Integer e) ,这里指定Integer类型

  1. 泛型的类型必须是类,不能是基本数据类型,需要基本类型的可用包装类替换
  2. 没有指明泛型类型时默认类型为java.lang.Object类型

自定义泛型

  1. 泛型的声明

interface List和 class GenTest<K,V…… >

其中,T,K,V 不代表值,而是表示类型。 这里使用任意字母都可以 。常用T 表示,是 Type 的缩写。

  1. 泛型的实例化

一定要在类名后面指定类型参数的值(类型)。如:

List<String> strList = new ArrayList <String>();
Iterator<Customer> iterator = customers.iterator();
  • T 只能是类,不能用基本数据类型填充。但可以使用包装类填充
  • 把一 个集合中的内容限制为一个特定的数据类型,这就是 generics 背后的核心思想
//JDK1.5之前
Comparable c = new Date();
System.out.println(c.compareTo("red"));

//JDK1.5
Comparable<Date> c = new Date();
System.out.println(c.compareTo("red"));

体会:使用泛型的主要优点是能够在编译时而不是在运行时检测错误。

class GenericTest {
	public static void main(String[] args )
        //1 、使用时:类似于 Object ,不等同于 Object
        ArrayList list = new ArrayList();
        //list.add(new Date();// 有风险
        list.add ("hello");
        test(list);// 泛型擦除,编译不会类型检查
        //ArrayList<Object> list2 = new ArrayList<Object>();
        //test(list2);// 一旦指定 Object ,编译会类型检查,必须按照 Object 处理
	}
    public static void test(ArrayList<String> list )
        String str = "";
        for(String s : list ){
            str += s + ",";
        }
        System.out.println("元素 :" + str);
    }
}

静态方法中不能使用类的泛型。(原因:泛型类型是实例化时指定,static类加载比其早 )

异常类不能是泛型的

父类有泛型,子类可以选择保留泛型也可以选择指定泛型类型:

  • 子类不保留父类的泛型:按需实现
    • 没有类型 擦除
    • 具体类型
  • 子类保留父类的泛型:泛型子类
    • 全部保留
    • 部分保留

结论:子类除了指定或保留父类的泛型,还可以增加自己的泛型

class Father<T1, T2> {
}
//子类不保留父类的泛型
// 1)没有类型 擦除
class Son1 extends Father { {// 等价于 class Son extends Father<Object,Object>{
}
// 2)具体类型
class Son2 extends Father<Integer, String> {
}
//子类保留父类的泛型
// 1)全部保留
class Son3<T1, T2> extends Father<T1, T2> {
}
// 2)部分保留
class Son4<T2> extends Father<Integer, T2> {
}
class Person<T> {
    // 使用 T 类型定义变量
    private T info;
    //使用 T 类型定义一般方法
    public T getInfo() {
    	return info;
	}
    public void setInfo(T info )
    	this.info = info;
    }
    //使用 T 类型定义构造器
    public Person() {
    }
    public Person(T info ){
        this.info = info;
    }

}
//实例化时不指定,则泛型类型为Object类型
Person person = new Person();
//指明类的泛型    
Person<String> person = new Person<String>();    

泛型方法的格式

[访问权限] <泛型> 返回类型 方法名 ([泛型标识 参数名称]) 抛出的异常

public class DAO {
    //泛型方法,<E>为标识,让编译器知道这是泛型而不是定义的类
    public <E> List<E> copyFrom(E[] arr)
        ArrayList<E> list = new ArrayList<>();
       for(E e : arr) {
           list.add(e);
       }
        return list;
    }
}

泛型 方法可以声明为static的。原因:泛型参数是在调用方法时确定的,并非在实例化类时确定的。静态方法是通过类名直接调用,此时根据传入的参数确定类型,符合静态方法初衷。(还是有点没理解)

通配符

使用类型通配符

比如:List<?>、 Map<?,?>, List<?>是 List 、 List 等各种泛型 List 的父类。

  • 考虑List 、 List, 原来String,Object是子父类关系,具有多态性,而加入泛型后导致多态性消失,所以引入了List<?>, 加强通用性
  • 读 取 List<?> 的对象 list 中的元素时,永远是安全的,因为不管 list 的真实类型是什么,它包含的都是 Object(根父类)
  • 写入 list 中的元素时,不行。因为我们不知道元素类型,无法向其中添加对象。(null除外,所有类型的成员)
Collection<?> c = new ArrayList<String>();
c.add(new Object()); // 编译时错误,不知道c 的元素类型,无法向其中添加对象
  • 有限制条件的通配符

    • <? extends A > :指定上限,可以是A的子类,最大不超过本身

    • <? super A> : 指定下限,可以是A的父类,最小不低于本身
    • <? extends Comparable>: 只允许泛型为实现 Comparable 接口的实现类的引用调用

IO流

File类

  • File 能新建、删除、重命名文件和目录,但 File 不能访问文件内容本身。如果需要访问文件内容本身,则需要使用输入/输出流

常用构造器

  • public File(String pathname)
    以pathname 为路径创建 File 对象,可以是 绝对路径或者相对路径 ,如果**pathname 是相对路径,则默认的当前路径在系统属性 user.dir(用户当前工作目录) 中存储。**如果 pathname 是实际存在的路径,则该 File 对象表示的是目录;如果 path 是文件名,则该 File 对象表示的是文件。

    • 绝对路径: 是一个固定的路径从盘符开始

    • 相对路径: 是相对于某个位置开始

  • public File(String parent,String child)
    以parent 为父路径, child 为子路径(文件名)创建 File 对象

    File file = new File("src/com/xiong/base/day10","test1.txt");
    try {
          	file.createNewFile();
    } catch (IOException e) {
            e.printStackTrace();
    }
    

在这里插入图片描述

  • public File(File parent,String child)
    根据一个父File 对象和子文件路径创建 File 对象
  • 路径分隔符和系统有关:
    • windows 和 DOS 系统默认使用“ \”来表示
    • UNIX 和 URL 使用“ / ”来表示
  • File 类提供了一个常量:public static final String separator。根据操作系统,动态的提供分隔符
/**
* The system-dependent default name-separator character.  This field is
* initialized to contain the first character of the value of the system
* property <code>file.separator</code>.  On UNIX systems the value of this
* field is <code>'/'</code>; on Microsoft Windows systems it is <code>'\\'</code>.
*
* @see     java.lang.System#getProperty(java.lang.String)
*/
public static final char separatorChar = fs.getSeparator();

/**
* The system-dependent default name-separator character, represented as a
* string for convenience.  This string contains a single character, namely
* <code>{@link #separatorChar}</code>.
*/
//File的静态属性String separator存储了当前系统的路径分隔符
public static final String separator = "" + separatorChar;
File file1 = new File("d:\\atguigu info.txt");
File file2 = new File(" d:"+ File.separator + " atguigu"+ File.separator +"info.txt");
File file3 = new File(File("d:/atguigu");

常用方法:

//获取
public String getAbsolutePath() //获取绝对路径
public String getPath() //获取路径
public String getName() //获取名称
public String getParent() //获取上层文件目录路径。若无返回 null
public long length() //获取文件长度即:字节数。不能获取目录的长度 
public long lastModified() //获取最后一次的修改时间毫秒值
public String[] list() //获取指定目录下的所有文件或者文件目录的名称数组
public File[] listFiles() //获取 指定目录下的所有文件或者文件目录的File数组
    
//重命名    
public boolean renameTo(File dest) //把文件重命名为指定的文件路径  

//判断
public boolean isDirectory() //判断是否是文件目录
public boolean isFile() //判断是否是文件
public boolean exists() //判断是否存在
public boolean canRead() //判断是否可读
public boolean canWrite() //判断是否可写
public boolean isHidden() //判断是否隐藏    
    
//创建  
public boolean createNewFile() //创建文件,若文件存在,则不创建,返回 false
public boolean mkdir() //创建文件目录,如果此文件目录存在, 就不创建,如果此文件目录的上层目录不存在 也不创建
public boolean mkdirs() //创建文件目录,如果上层文件目录不存在, 一并创建
//注意事项:如果你创建文件或者文件, 目录没有写盘符路径, 那么默认在项目路径下
    
//删除
public boolean delete() //删除文件或者文件夹。注:Java中的删除不走回收站,要删除一个文件目录,请注意该文件目录内不能包含文件或者文件目录    

在这里插入图片描述

File dir1 = new File(File("D:/IOTest/");
if(!dir1 .exists(){ // 如果 D:/IOTest/dir1 不存在,就创建为目录
	dir1.mkdir();
}
   
//创建以 dir1 为父目录 名为 "dir2" 的 File 对象
File dir2 = new File( dir1 , "dir2");
if(!dir2.exists()) { // 如果还不存在,就创建为目录
	dir2.mkdirs();
}
   
Filedir4 = new File( dir1 , "dir3/dir4");
if(!dir4 .exists()){
	dir4.mkdirs();
}
//创建以 dir2 为父目录 名为 "test.txt"的 File 对象
File file = new File( dir2 , "test.txt");
if(!file .exists()) { // 如果还不存在,就创建为文件
	file.createNewFile();
}

I/O流

Java 程序中,对于数据的输入/输出操作以 “流(stream ) "的方式进行; java.io 包下提供了各种“流”类和接口,用以获取不同种类的数据,并通过 标准的方法 输入或输出数据。

在这里插入图片描述

  • 输入 input 读取外部数据(磁盘、光盘等存储设备的数据)到程序(内存)中
  • 输出 output 将程序(内存)数据输出到磁盘、光盘等 存储设备中
流的分类
  • 按操作数据单位不同分为: 字节流 (8 bit),字符流 (16bit)

  • 按数据流的流向不同分为: 输入流,输出流

  • 按流的 角色 的不同分为: 节点流,处理流

    • 节点流:直接从数据源或目的地读写数据
      在这里插入图片描述

    • 处理流:不直接连接到数据源或目的地,而是“连接” 在已存在的流(节点流或处理流)之上,通过对数据的处理为程序提供更为强大的读写功能。

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

I/O流体系
在这里插入图片描述

字节流和字符流
  • 字节流

    Java中的字节流处理的最基本单位为单个字节,通常用来处理二进制数据(非文本数据)。

    字节流在默认情况下是不支持缓存的,这意味着每调用一次read方法都会请求操作系统来读取一个字节,这往往会伴随着一次磁盘IO,因此效率会比较低。

  • 字符流

Java中的字符流处理的最基本的单元是Unicode码元(大小2字节),通常用来处理文本数据。所谓 Unicode码元,也就是一个Unicode代码单元,范围是0x0000~0xFFFF。在以上范围内的每个数字都与一 个字符相对应,Java中的String类型默认就把字符以Unicode规则编码而后存储在内存中。然而与存储在 内存中不同,存储在磁盘上的数据通常有着各种各样的编码方式。使用不同的编码方式,相同的字符会 有不同的二进制表示。实际上字符流是这样工作的:

输出字符流:把要写入文件的字符序列(实际上是Unicode码元序列)转为指定编码方式下的字节序列,然后再写入到文件中;

输入字符流:把要读取的字节序列按指定编码方式解码为相应字符序列(实际上是Unicode码元序列从)从而可以存在内存中。

由于字符流在输出前实际上是要完成Unicode码元序列到相应编码方式的字节序列的转换,所以它会使用内存缓冲区来存放转换后得到的字节序列,等待都转换完毕再一同写入磁盘文件中。

字节流和字符流的区别

字节流与字符流之间主要的区别体现在以下几个方面:

  • 字节流操作的基本单元为字节;字符流操作的基本单元为Unicode码元。
  • 字节流默认不使用缓冲区;字符流使用缓冲区。
  • 字节流通常用于处理二进制数据,实际上它可以处理任意类型的数据,但它不支持直接写入或读取Unicode码元;字符流通常处理文本数据,它支持写入及读取Unicode码元。
  • InputStream (典型实现: FileInputStream

    • int read()
    • int read(byte[] b)
    File file = new File("hello.txt");
    File srcFile = new File("1.jpg");
    File destFile = new File("2.jpg");
    FileInputStream fs = new FileInputStream(file);
    FileInputStream fis = new FileInputStream(srcFile);
    FileOutputStream fos = new FileOutputStream(destFile);
    byte[] buffer = new byte[5];
    int len; 
    //读文本数据,直接复制到文本中,中文字符会重新拼接(不会乱码),如果输出到控制台,可能出现乱码
    while((len = fs.read(buffer)) != -1) {
        for(int i=0; i<len; i++) { //这里长度要用返回的len,否则会导致覆盖问题
            System.out.println(buffer[i]); //中文字符会乱码,一个中文3字节(utf8),读取时										可能会被截断,只读到前1个或两个字节,导致输出乱码
        }
    }
    //复制图片数据(非文本数据)
    byte[] buffer = new byte[5];
    while((len = fis.read(buffer)) != -1) {
        fos.write(buffer,0,len);
    }
    fs.close();
    fis.close();
    fos.close();
    
    • int read(byte[] b, int off, int len)
  • Reader (典型实现 FileReader

    • int read() 读取单个字符,返回一个int型变量代表读取到的字符,范围在 0 到 65535 之间 (0x00-0xffff)( 2 个字节的 Unicode 码), 如果已到达流的末尾, 则返回 -1.
    public class FileReaderDemo {
        public static void main(String[] args) {
             //1. File类的实例化
             File file = new File("hello.txt"); //向较于当前工程下
            FileReader fr = null;
            //为保证流资源一定可执行关闭操作,需try-catch-finally处理
            try {
                //2.流的实例化
                fr = new FileReader(file);
                //3.数据的读入
                int data;
                while((data = fr.read()) != -1) {
                    System.out.println((char) data);
                }
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }finally {
                try {
                    if (fr != null) {//可能在创建时出现异常
                        //4.关闭流
                        //程序中打开的文件 IO 资源不属于内存里的资源,垃圾回收机制无法回收该						资源,所以应该显式关闭文件 IO 资源 。
                        fr.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    • int read(char [] c) 读取字符存到c数组,返回每次读入c数组中的字符个数,达到末尾,返回-1
    • int read(char [] c, int off, int len)
char[] cbuf = new char[5];
int len;
while((len = fr.read(cbuf)) != -1) {
    for(int i=0; i<len; i++) { //这里长度要用返回的len,否则会导致覆盖问题
        System.out.println(cbuf[i]);
    }
}

FileInputStream 从文件系统中的某个文件中获得输入字节。 FileInputStream用于读取非文本数据之类的原始字节流。要读取字符流需要使用 FileReader

  • Writer

    FileWriter fileWriter = null;
    //1.提供File类对象
    File file = new File("demo.txt"); //不存在就新建文件,存在就覆盖
    //2.提供FileWriter的对象,用于数据写入 
    fileWriter = new FileWriter(file,true);//ture:在末尾追加,false:覆盖
    //3.写入
    fileWriter.write("I am javaer");
    //关闭流
    fileWriter.close();
    }
    

    以上代码中,我们使用FileWriter向demo.txt中写入了“demo”这四个字符,我们用十六进制编辑器WinHex查看下demo.txt的内容:

在这里插入图片描述

 从上图可以看出,我们写入的“demo”被编码为了“64 65 6D 6F”,但是我们并没有在上面的代码中显式指定编码方式,实际上,在我们没有指定时使用的是操作系统的默认字符编码方式来对我们要写入的字符进行编码。
缓冲流

为了提高数据读写的速度, Java API 提供了带缓冲功能的流类,在使用这些流类时,会创建一个内部缓冲区数组,缺省使用 8192 个 字节 (8Kb) 的缓冲区

private static int DEFAULT_BUFFER_SIZE = 8192;

缓冲流要“套接”在相应的节点流之上,根据 数据操作单位可以把缓冲流分为:

  • BufferedInputStreamBufferedOutputStream
  • BufferedReaderBufferedWriter

当读取数据时,数据按块读入缓冲区,其后的读操作则直接访问缓冲区.

  • 当 使用 BufferedInputStream 读取字节文件时, BufferedInputStream 会一次性从文件中读取 8192 个 (8Kb) ,存在缓冲区中. 直到缓冲区装满了才重新从文件中读取下一个 8192 个字节 数组 。
  • 向流中写入字节时 , 不会直接写到文件, 先写到缓冲区中直到缓冲区写满,BufferedOutputStream 才会把缓冲区中的数据一次性写到文件里 。使用方法flush() 可以强制将缓冲区的内容全部写入输出流
  • 关闭流的顺序和打开流的顺序相反。只要关闭最外层流即可, 关闭最外层流也会相应关闭内层节点流

在这里插入图片描述
在这里插入图片描述

BufferedReader br = null;
BufferedWriter bw = null;
try{
    //创建缓冲流对象:它是处理流 ,是对节点流的包装,所以先造节点流,再造缓冲流
    br = new BufferedReader( new FileReader(FileReader(" IOTest source.txt"));
    bw = new BufferedWriter( new FileWriter(FileWriter(" IOTest dest.txt"));
    String str;
    
    while((str = br .readLine()) != null ) { // 一次读取字符文本文件的一行字符
        bw.write(str); // 一次写入一行字符串
        bw.newLine(); // 写入行分隔符 
    }
    bw.flush(); //刷新(清空)缓冲区,不需要是被动的等待输出缓冲区写入
} catch (IOException e ){
	e.printStackTrace();
}
finally{
//关闭 IO 流对象
    try {
        if(bw != null){
            bw.close(); // 关闭处理流时 会自动关闭它所包装的底层节点流
        }
    } catch (IOException e ){
        e.printStackTrace();
    }
    try{
        if(br != null ){
            br.close();
        }
    }
    catch (IOException e ){
        e.printStackTrace();
    }
}

在这里插入图片描述

转换流

转换流提供了在字节流和字符流之间的转换

  • InputStreamReader :将 InputStream 转换为 Reader
//构造器
public InputStreamReader (InputStream in)
public InputSreamReader (InputStream in,String charsetName)
//例,指定字符集
Reader isr = new InputStreamReader(System.in,"gbk");
  • OutputStreamWriter :将 Writer 转换为 OutputStream
//构造器
public OutputStreamWriter (OutputStream out)
public OutputSreamWriter (OutputStream out,String charsetName)

字节流中的数据都是字符时,转成字符流操作更高效 。使用转换流来处理文件乱码问题可实现编码和解码
在这里插入图片描述

对象流

ObjectInputStreamOjbectOutputSteam,用于存储和读取基本数据类 数据或对象的处理流。可以把 Java 中的对象写入到数据源中,也能把对象从数据源中还原回来。

  • 序列化: 用 ObjectOutputStream 类保存基本类型数据或对象的机制
  • 反序列化: 用 ObjectInputStream 类读取基本类型数据或对象的机制

ObjectOutputStream 和 ObjectInputStream 不能序列化 statictransient 修饰的成员变量

对象序列化

  • 对象序列化机制 允许把内存中的 Java 对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络 节点 。 当其它程序获取了这种二进制流,就可以恢复成原来的 Java 对象

  • 序列化好处在于可将任何实现了 Serializable 接口的对象转化为 字节数据,使其在保存和传输时可被还原

  • 序列化是 RMI (Remote Method Invoke — 远程方法调用)过程的参数和返回值都必须实现的机制,而 RMI 是 JavaEE 的基础。因此序列化机制是JavaEE 平台的基础

    • 如果需要让某个对象支持序列化机制,则必须让对象所属的类及其属性是可序列化的,为了让 某个类是可序列化的,该类 必须实现如下两个接口之一。否则,会抛出 NotSerializableException 异常
    • Serializable
    • Externalizable
  • 凡是实现 Serializable 接口的类都有一个表示序列化版本标识符的静态变量:

    • private/public static final long serialVersionUID;
    • serialVersionUID 用来表明类的不同版本间的兼容性。 简言之,其目的是以序列化对象进行版本控制,有关各版本反序列化时是否兼容。
    • 如果类没有显示定义这个静态常量 ,它的值是 Java 运行时环境根据类的内部细节自动生成的 。 若类的实例变量做了修改 serialVersionUID 可能发生变化。 故建议显式声明。
  • Java 序列化机制是通过在运行时判断类的serialVersionUID 来验证版本一致性的。进行反序列化时,JVM 会把传来的字节流中的serialVersionUID 与本地相应实体类的 serialVersionUID 进行比较,如果相同就认为一致,可以进行反序列化,否则就会出现序列化版本不一致的异常。(InvalidCastException)

  • 使用对象流序列化对象

  1. 创建一个 ObjectOutputStream
  2. 调用 ObjectOutputStream 对象的 writeObject (对象 ) 方法输出可序列化对象
  3. 注意写出一次,操作 flush() 一次
//序列化:将 对象写入到磁盘或者进行网络传输。
//要求 对象必须实现序列化
ObjectOutputStream oos = new ObjectOutputStream( new FileOutputStream ("data.txt"));
Person p = new Person( 韩梅梅 "", 18, 中华大街 "", new Pet());
oos.writeObject(p);
oos.flush();
oos.close();
  • 反序列化

    1. 创建一个 ObjectInputStream
    2. 调用 readObject () 方法读取流中的对象

    如果某个类的属性不是基本数据类型或 String 类型,而是另一个引用类型,那么这个引用类型必须是可序列化的,否则拥有该类型的Field 的类也不能序列化

//反序列化:将磁盘中的对象数据源读出
ObjectInputStream ois = new ObjectInputStream( new FileInputStream ("data.txt"));
Person p1 = (Person)ois.readObject();
System.out.println(p1.toString());
ois.close();

java.io.Serializable

实现了 Serializable 接口的对象,可将它们转换成一系列字节,并可在以后完全恢复回原来的样子。 这一过程亦可通过网络进行。这意味着序列化机制能自动补偿操作系统间的差异。 换句话说,可以先在 Windows 机器上创建一个对象,对其序列化,然后通过网络发给一台 Unix 机器,然后在那里准确无误地重新“装配”。不必关心数据在不同机器上如何表示,也不必关心字节的顺序或者其他任何细节。

随机存取文件流

RandomAccessFile 声明在 java.io 包下,但直接继承于 java.lang.Object 类。 并且它实现了 DataInput 、 DataOutput 这两个接口,也就意味着这个类既可以读也可以写。RandomAccessFile 类支持 随机访问 的方式,程序可以直接跳到文件的任意地方来 读、写文件。

  • 支持只访问文件的部分内容
  • 可以向已存在的文件后追加内容

RandomAccessFile 对象包含一个记录指针,用以标示当前读写处的位置。RandomAccessFile 类对象可以自由移动记录指针:

  • long getFilePointer(): 获取文件记录指针的当前位置
  • void seek(long pos) : 将文件记录指针定位到 pos 位置

构造器

//mode为指定访问模式,r: 以只读方式打开;rw :打开以便读取和写入;rwd 打开以便读取和写入,同步文件内容的更新(写入硬盘);rws 打开以便读取和写入,同步文件内容和元数据的更新
public RandomAccessFile (File file , String mode)

public RandomAccessFile (String name, String mode)

如果模式为只读 r 。则不会创建文件,而是会读取一个已经存在的文件,如果读取的文件不存在则出现异常。 模式为 rw 读写时,如果文件不存在则会去创建文件,如果存在则不会创建(可能会出现覆盖问题,从索引位置开始覆盖)。

RandomAccessFile raf = new RandomAccessFile ("test.txt", "rw");
raf.seek(5);
byte[] b = new byte[1024];
//读写文件
int off = 0;
int len = 5; 
raf.read(b, off, len);
String str = new String(b, 0, len);
System.out.println(str);

//先读出来,再移动指针,实现插入效果
String temp = raf.readLine();
raf.seek(5);//将指针指到5的位置
raf.write("xyz".getBytes());
raf.write(temp.getBytes());

raf.close();

处理数据时,一定要先明确 数据源 src,与 数据目标dest

NIO概述

NIO支持面向缓冲区的 (IO 是面向流的) 、基于通道的 IO 操作。 NIO 将以更加高效的方式进行文件的读写操作。提供了两套 NIO 一套是针对标准输入输出 NIO, 另一套就是网络编程 NIO。

|---- java.nio.channels.Channel
	--- FileChannel: 处理本地文件
	--- SocketChannel: TCP网络编程的客户端的 Channel
	--- ServerSocketChannel: TCP网络编程的服务器端的 Channel
    --- DatagramChannel: UDP网络编程中发送端和接收端的 Channel

File 类大多数方法在出错时仅返回失败,并不会提供异常信息。NIO. 2 为了弥补这种不足,引入了 Path 接口,代表一个平台无关的平台路径,描述了目录结构中文件的位置。 Path 可以看成是 File 类的升级版本,实际引用的资源也可以不存在。

//在以前 IO 操作都是这样写的
import java.io.File;
File file = new File("index.html");

//但在 Java7 中,我们可以这样写:
import java.nio.file.Path;
import java.nio.file.Paths;
Path path = Paths.get("index.html");

//Paths 类提供的静态get()方法用来获取 Path 对象
static Path get(String first, String … more) : 用于将多个字符串串连成路径

static Path get(URI uri): 返回指定 uri 对应的 Path 路径

Path常用方法

Path toAbsolutePath() : 作为绝对路径返回调用Path对象
File toFile():Path转化为File类的对象
  • java.nio.file.Files 用于操作文件或目录的工具类

Files常用方法

Path createDirectory(Path path, FileAttribute<?> … attr) : 创建一个目录

Path createFile(Path path, FileAttribute<?> … arr) : 创建一个文件
void deleteIfExists(Path path) : Path 对应的文件 目录如果存在,执行删除
long size(Path path) : 返回 path 指定文件的大小

网络编程

TCP/IP协议

传输层协议

  • 传输控制协议 TCP(Transmission Control Protocol)

    • 使用 TCP 协议前,须先建立 TCP 连接,形成传输数据通道
    • 传输前,采用 三次握手 方式 ,点对点通信是 可靠
    • TCP 协议进行通信的两个应用进程:客户端、 服务端。
    • 在连接中可进行大数据量的传输
    • 传输完毕,需释放已建立的连接效率低

在这里插入图片描述
在这里插入图片描述

  • 用户数据报协议 UDP(User Datagram Protocol)

    • 将数据、源地址、目的地址封装成数据包, 不需要建立连接
    • 每个数据报的大小限制在 64K
    • 发送不管对方是否准备好,接收方收到也不确认, 故是 不可靠
    • 可以广播发送
    • 发送数据结束时 无需释放 资源 ,开销小,速度快

网络层协议

  • IP(Internet Protocol) 协议是网络层的主要协议,支持网间互连的数据通信。
    • InternetAddress类

Socket

网络上具有唯一标识的 IP 地址和端口号组合在一起才能构成唯一能识别的标识符套接字

  • 网络通信其实就是 Socket 间的通信。

  • Socket 允许程序把网络连接当成一个流, 数据在两个 Socket 间通过 IO 传输。一般主动发起通信的应用程序属 客户端 ,等待通信 请求的为 服务端

  • Socket 分类:

    • 流套接字( stream socket ):使用 TCP 提供可依赖的字节流服务
    • 数据报套接字( datagram socket ):使用 UDP 提供“尽力而为”的数据报服务
  • 常用构造器

public Socket(InetAddress address,int port) :创建一个流套接字并将其连接到指定 IP 地址的指定端口号 。

public Socket(String host,int port) :创建一个流套接字并将其连接到指定主机上的指定端口号

TCP网络编程

基于Socket的TCP编程

在这里插入图片描述

  • 客户端 Socket 的工作过程包含以下四个基本的步骤
    • 创建 Socket: 根据指定服务端的 IP 地址或端口号构造 Socket 类对象。若服务器端响应,则建立客户端到服务器的通信线路。若连接失败,会出现异常。创建的同时会自动向服务器方发起连
      接。
    • 打开连接到 Socket 的输入/出流: 使用 getInputStream 方法获得输入流,使用getOutputStream 方法获得输出流,进行数据传输
    • 按照一定的协议对 Socket 进行读写操作: 通过输入流读取服务器放入线路的信息(但不能读取自己放入线路的信息),通过输出流将信息写入线程。
    • 关闭 Socket: 断开客户端到服务器的连接,释放线路
Socket s = new Socket( "192.168.40.165",9999);
OutputStream out = s.getOutputStream();
out.write(" hello".getBytes());
s.close();
  • 服务器程序的工作过程包含以下四个基本的步骤
    • 调用 ServerSocket (int port ): 创建 一个服务器端套接字,并绑定到指定端口上 。用于监听客户端的请求。服务器必须事先建立一个等待客户请求建立套接字连接的 ServerSocket 对象。
    • 调用 accept() :监听连接请求,如果客户端请求连接,则接受连接,返回通信套接字对象 。
    • 调用该 Socket 类对象的 getOutputStream () 和 getInputStream() :获取输出流和输入流,开始网络数据的发送和接收。
    • 关闭 ServerSocket 和 Socket 对象: 客户端访问结束,关闭通信套接字 。
ServerSocket ss = new ServerSocket(9999);
Socket s = ss.accept();
InputStream in = s.getInputStream();
byte[] buf = new byte[1024];
int num = in.read(buf);
String str = new String(buf,0,num);
System.out.println(s.getInetAddress().toString()+":"+str);
s.close();
ss.close();

在这里插入图片描述

UDP网络通信

  • DatagramSocketDatagramPacket 实现了基于UDP 协议网络程序。
  • 类 DatagramSocket 和 DatagramPacket 实现了基于 UDP 协议网络程序。UDP 数据报通过数据报套接字 DatagramSocket 发送和接收, 系统不保证UDP 数据报一定能够安全送到目的地,也不能确定什么时候可以抵达。
  • DatagramPacket 对象封装了 UDP 数据报,在数据报中包含了发送端的 IP地址和端口号以及接收端的 IP 地址和端口号。
  • UDP 协议中每个数据报都给出了完整的地址信息,因此无须建立发送方和接收方的连接。 如同发快递包裹一样

流程

  1. DatagramSocket 与 DatagramPacket
  2. 建立发送端,接收端
  3. 建立数据包
  4. 调用 Socket 的发送 、 接收方法
  5. 关闭 Socket
//发送端
DatagramSocket ds = null;
try {
    ds = new DatagramSocket();
    byte[] by = "hello,xiong.com".getBytes();
    DatagramPacket dp = new DatagramPacket( by , 0, by.length ,
                        InetAddress.getByName ("127.0.0.1"),10000);
	ds.send(dp);
}
catch (Exception e )
	e.printStackTrace();
} finally {
    if(ds != null){
        ds.close();
    }
}

//接收端,要指定监听的端口 
DatagramSocket ds = null;
try {
    ds = new DatagramSocket(10000);
    byte[] by = new byte [1024];
    DatagramPacket dp = new DatagramPacket( by , by.length);
    ds.receive(dp);
    String str = new String( dp.getData(), 0, dp.getLength());
    System.out.println(str + ""----""+ dp.getAddress);
} catch (Exception e ){
	e.printStackTrace();
} finally{
    if(ds != null){
        ds.close();
    }
}

URL类

URL(Uniform Resource Locator):统一资源定位符,它表示 Internet上某一资源的地址(具体的URI,可以标识一个资源)

  • 基本结构

<传输协议>://<主机名>:<端口号>/<文件名>#片段名?参数列表

片段名:即锚点,例如看小说,直接定位到章节。片段URL表示网页的一个位置

任一带#的URL称为片段URL(ps:通常称为URL hash,下文统一称为URL hash)。#左边部分是浏览器可以下载的资源,#右边部分称为片段标识符,表示资源内的某一位置。

在这里插入图片描述

在HTML文档里,浏览器会寻找name属性为print的标签,找到后滚动页面到该位置进行展示,如下图所示:

在这里插入图片描述

参数列表格式:参数名=参数值&参数名=参数值……

  • 构造器
public URL (String spec ):通过一个表示URL地址的字符串可以构造一个URL对象
//例如 URL url = new URL ("http://www. atguigu.com/");

public URL(URL context, String spec ):通过基URL和相对URL构造一个URL对象
//例如 URL downloadUrl = new URL(url , "download.html");

public URL(String protocol, String host, String file); 
//例如 new URL("http","www.atguigu.com", "download.html");

public URL(String protocol, String host, int port, String file);
//例如 : URL gamelan = new URL ("http", "www.atguigu.com", 80, "download.html");
  • URL 类的构造器都声明抛出非运行时异常,必须要对这一异常进行处理,通常是用 try-catch 语句进行捕获。
  • 一 个 URL 对象生成后,其属性是不能被改变的,但可以通过它给定的方法来获取这些属性:
public String getProtocol( ) //获取该 URL 的协议名

public String getHost( ) //获取该 URL 的主机名

public String getPort( ) //获取该 URL 的端口号

public String getPath( ) //获取该 URL 的文件路径

public String getFile( ) //获取该 URL 的文件名

public String getQuery( ) //获取该 URL 的查询名
URLConnection类

URL 的方法 openStream():能从网络上读取数据

  • URLConnection :表示到 URL 所引用的远程对象的连接。当与一个 URL 建立连接时,首先要在一个 URL 对象上通过方法 openConnection () 生成对应的 URLConnection对象。如果连接过程失败,将产生 IOException .
    • URL netchinaren = new URL (“http://www.atguigu.com/index.shtml”);
    • URLConnectonn u = netchinaren.openConnection( );
  • 通过 URLConnection 对象获取的输入流和输出流,即可以与现有的 CGI程序进行交互。
public Object getContent() throws IOException

public int getContentLength()

public String getContentType()

public long getDate()

public long getLastModified()

public InputStream getInputStream() throws IOException

public OutputSteram getOutputStream() throws IOException
URI、 URL 和 URN 的区别

URI,是 uniform resource identifier ,统一资源标识符 用来唯一的标识一个资源。而 URL 是 uniform resource locator ,统一资源定位符 ,它是一种具体的 URI ,即 URL 可以用来标识一个资源,而且还指明了如何 locate 这个资源。而 URN uniform resource name ,统一资源命名,是通过名字来标识资源。比如 mailto:java net@java.sun.com 。也就是说, URI 是以一种抽象的,高层次概念定义统一资源标识,而 URL 和 URN 则是具体的资源标识的方式。 URL和 URN 都是一种 URI。

在这里插入图片描述

总结

  • 客户端-服务器 (C/S)是一种最常见的网络应用程序模型。服务器是一个为其客户端提供某种特定服务的硬件或软件。客户机是一个用户应用程序,用于访问某台服务器提供的服务。 端口号是对一个服务的访问场所,它用于区分同一物理计算机上的多个服务。 套接字用于连接客户端和服务器,客户端和服务器之间的每个通信会话使用一个不同的套接字。 TCP 协议用于实现面向连接的会话。
  • Java 中有关网络方面的功能都定义在 java.net 程序包中。 Java 用 InetAddress 对象表示 IP地址 ,该对象里有两个字段:主机名 (String) 和 IP 地址 (int)。
  • 类 Socket 和 ServerSocket 实现了基于 TCP 协议的客户端-服务器程序。 Socket 是客户端和服务器之间的一个连接,连接创建的细节被隐藏了。这个连接提供了一个安全的数据传输通道,这是因为 TCP 协议可以解决数据在传送过程中的丢失、损坏、重复、乱序以及网络拥挤等问题,它保证数据可靠的传送
  • 类 URL 和 URLConnection 提供了最高级网络应用。 URL 的网络资源的位置来同一表示Internet 上各种网络资源。通过 URL 对象可以创建当前应用程序和 URL 表示的网络资源之间的连接,这样当前程序就可以读取网络资源数据,或者把自己的数据传送到网络上去。

反射

Reflection (反射)是被视为动态语言的关键,反射机制允许程序在执行期借助于 Reflection API 取得任何类的内部信息,并能直接操作任意对象的内部属性及方法.

  • 加载完类之后 在堆内存的方法区中就产生了一个 Class 类型的对象( 一个类只有一个 Class 对象), 这个对象就包含了完整的类的结构信息 。 我们可以通过这个对象看到类的结构 。 这个对象就像一面镜子 透过这个镜子看到类的结构, 所以我们形象的称之为: 反射

在这里插入图片描述

  • Java 反射机制提供的功能
    • 在运行时判断任意一个对象所属的类
    • 在运行时构造任意一个类的对象
    • 在运行时判断任意一个类所具有的成员变量和方法
    • 在运行时获取泛型信息
    • 在运行时调用任意一个对象的成员变量和 方法
    • 在 运行时处理注解
    • 生成动态代理
  • 反射相关的主要API
java.lang.Class 代表一个类

java.lang.reflect.Method 代表类的方法

java.lang.reflect.Field 代表类的成员变量

java.lang.reflect.Constructor 代表类的构造器

Class类

在Object 类中定义了以下的方法,此方法将被所有子类继承:

  • public final Class getClass()

以上的方法返回值的类型是一个Class 类,此类是 Java反射的源头。

在这里插入图片描述

Class 本身也是一个类, 只能 由系统 建立 对象;一个加载的类 在 JVM 中 只会有一个 Class 实例;

一 个Class对象对应的是一个加载到 JVM 中的一个 class 文件;每个类的实例都会记得自己是由哪个 Class实例所生成;

通过 Class 可以完整地得到一个类中的所有被加载的结构Class 类是 Reflection 的根源,针对任何你想动态加载、运行的类,唯有先获得相应的Class 对象;

Class类的常用方法

在这里插入图片描述

反射之前,对于 Person类的操作

//创建Person类对象实例
Person p1 = new Person("Tom", 12);
//通过对象,调用内部属性、方法
p1.age = 10;
System.out.println(p1.toString());//调用person类的tostring()方法

p1.show();

在Person类外部,不可以通过Person类的对象调用其内部私有结构。

反射之后,对类操作

Class clazz = Person.class;
//通过反射,创建Person类对象
Construcor cons = clazz.getConstrucor(string.class , int.class);//构造器参数
//调用public构造器创建实例
Object obj = clazz.newInstance("Tom",12);

Person p = (Person) obj;
//通过反射,调用对象指定的属性、方法
//调用public属性
Field field = clazz.getDeclaredField ("age"); 
field.set(p, 10);
//调用public方法,返回空参方法,(存在同名不同参方法,可通过传参获取)
Method show = clazz.getDeclaredMethod("show");
show.invoke(p); //调用

//调用私有构造器,私有属性,方法同
Construcor cons1 = clazz.getDeclaredConstrucor(string.class);//构造器参数
cons1.setAccessible(true);//私有方法调用后可访问私有方法
Person p1 = (Person)cons1.newInstance("jerry");

通过反射,可以调用类的私有结构

获取Class 类的实例 (四种方法)

  1. 前提: 若已知具体的类,通过类的 class 属性获取, 该 方法最为 安全可靠,程序性能最高
    实例 Class clazz = String.class;

  2. 前提: 已知某个类的实例,调用该实例的 getClass 方法 获取 Class 对象
    实例 Person p = new Person();

    ​ Class clazz = p.getClass();

  3. 前提: 已知一个类的全类名,且该类在类路径下, 可通过 Class 类的静态方法 forName 获取,可能抛出 ClassNotFoundException
    实例 Class clazz = Class.forName(“java.lang.String”);

  4. 使用类的加载器
    ClassLoader cl = this.getClass().getClassLoader();
    Class clazz4 = cl.loadClass(“类的全类名”);

类的加载过程

在这里插入图片描述

  • 加载:将 class 文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的 java.lang.Class 对象,作为方法区中类数据的访问入口(即引用地址)。所有需要访问和使用类数据只能通过这个 Class 对象。这个加载的过程需要类加载器参与。
  • 链接:将 Java 类的二进制代码合并到 JVM 的运行状态之中的过程。
    • 验证:确保加载的类信息符合 JVM 规范,例如:以 cafe 开头,没有安全方面的问题
    • 准备:正式为类变量( static )分配内存并设置类变量默认初始值 的阶段,这些内存
      都将在方法区中进行分配 。
    • 解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程 。
  • 初始化
    • 执行类构造器 () 方法的过程。 类构造器 () 方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。 (类构造器是构造类信息的,不是构造该类对象的构造器 。)
    • 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化 。
    • 虚拟机会保证一个类的 () 方法在多线程环境中被正确加锁和 同步

创建运行时类的对象 : 调用 Class 对象的 newInstance() 方法

要求: 1 )类必须有一个无参数的构造器,没有无参构造器则必须明确传递参数,使用 getDeclaredConstructor(Class … parameterTypes) 取得本类的指定形参类型的构造器。
2)类的构造器的访问权限需要足够。

**获取运行时类的完整结构:**Field、 Method 、 Constructor 、 Superclass 、 Interface 、 Annotation

//确定此对象所表示的类或接口实现的接口。
public Class<?>[] getInterfaces () 

//返回表示此Class 所表示的实体(类、接口、基本类型)的父类的Class
public Class<? Super T> getSuperclass()

//返回此Class 对象所表示的类的所有 public 构造方法。
public Constructor<T>[] getConstructors() 
//返回此Class 对象表示的类声明的所有构造方法。
public Constructor<T>[] getDeclaredConstructors() 
//取得修饰符 
public int getModifiers()
//取得方法名称 
public String getName()
//取得参数的类型
public Class<?>[] getParameterTypes()
   
//返回此Class 对象所表示的类或接口的全部方法,不包括其父类中的方法 
public Method[] getDeclaredMethods()
//返回此Class 对象所表示的类或接口的 public 的方法  ,包括其父类中的方法 
public Method[] getMethods ()
//Method类中    
public Class<?> getReturnType() //取得全部的返回值
public Class<?>[] getParameterTypes() //取得全部的参数
public int getModifiers() //取得修饰符
public Class<?>[] getExceptionTypes() //取得异常信息 

//返回此Class对象所表示的类或接口的public的Field     
public Field[] getFields ()
//返回此Class 对象所表示的类或接口的全部 Field,不包含父类的属性
public Field[] getDeclaredFields ()
//Field 方法中:
public int getModifiers () //以整数形式返回此 Field 的修饰符
public Class<?> getType () //得到 Field 的属性类型
public String getName () //返回 Field 的名称。

//Annotation相关
getAnnotation(Class<T> annotationClass )  
getDeclaredAnnotations ()    

//泛型相关    
//获取父类泛型类型
Type getGenericSuperclass()
//泛型类型
ParameterizedType()
//获取实际的泛型类型参数数组
getActualTypeArguments()

//类所在的包 
Package getPackage ()    

调用运行时类的指定结构

1. 调用指定方法

通过反射,调用类中的方法,通过Method 类完成。

在这里插入图片描述

  1. 调用指定属性

在反射机制中,可以直接通过Field 类操作类中的属性,通过 Field 类提供的 set() 和get() 方法就可以完成设置和取得属性内容的操作。

public Object get(Object obj ) //取得指定对象obj上此Field的属性内容
    
public void set(Object obj,Object value) //设置指定对象obj上此Field的属性内容

关于setAccessible 方法的使用

  • Method 和 Field 、 Constructor 对象都有 setAccessible() 方法 。
  • setAccessible 启动和禁用访问安全检查的开关 。
  • 参数值 为 true 则指示反射的对象在使用时应该取消 Java 语言访问检查 。
    • 提高反射的效率 。如果代码中必须用反射, 而该句代码需要频繁的被调用, 那么请设置为 true
    • 使得原本无法访问的私有成员也可以访问
    • 参数值为 false, 则指示反射的对象应该实施 Java 语言访问检查

反射和封装性

  1. 直接new的方式和反射方式都可以调用公共结构,一般用哪个

    一般使用new的方式(在编译时就已经确定下来了我们要创建哪个类的对象)

    反射的特征:动态性

    当我们无法确定需要new的对象时,使用反射机制,即动态获取对象

  2. 反射机制与面向对象中的封装性是否矛盾,如何看待

​ 封装性是建议不要调用,因为public的属性和方法已经能够完成整个类的功能了

​ 而反射解决的是能不能调的问题

类的加载与ClassLoader 的理解

在这里插入图片描述

类加载器的作用

  • 类加载的作用将 class 文件字节码内容加载到内存中 , 并将这些静态数据转换成方法区的运行时数据结构, 然后在堆中生成一个代表这个类的 java.lang.Class 对象, 作为方法区中类数据的访问入口 。
  • 类缓存: 标准的 JavaSE 类加载器可以按要求查找类, 但一旦某个类被加载到类加载器中,它将维持加载缓存 一段时间 。 不过 JVM 垃圾回收机制可以回收这些 Class 对象 。
    在这里插入图片描述

在这里插入图片描述

动态代理

代理设计模式的原理 :

使用一个代理将对象包装起来 , 然后用该代理对象取代原始对象任何对原始对象的调用都要通过代理。代理对象决定是否以及何时将方法调用转到原始对象上。(对应接口部分的静态代理 )

  • 动态代理是指客户通过代理类来调用其它对象的 方法,并且是在程序运行时根据需要动态创建目标类的代理对象。

  • 动态代理使用场合

    • 调试
    • 远程 方法 调用
  • 动态代理相比于静态代理的优点

    ​ 抽象角色中(接口)声明的所有方法都被转移到调用处理器一个集中的方法中处理,这样,我们可以更加灵活和统一的处理众多的方法。

  • Proxy :专门完成代理的 操作类,是所有动态代理类的父类。通过此类为一个或多个接口动态地生成实现类。

在这里插入图片描述

/*
*   动态代理
*
 */
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

interface Human {
    String getBelief();
    void eat(String food);
}

//2.创建被代理的类以及接口
// 被代理类
class SuperMan implements Human {

    @Override
    public String getBelief() {
        return "I can fly";
    }

    @Override
    public void eat(String food) {
        System.out.println("我喜欢吃:" + food);
    }
}

/*
    实现动态代理,需要解决的问题
    1. 如何根据加载到内存中的被代理类,动态创建一个代理类及其对象
    2. 当通过代理类的对象调用方法时,如何动态地去调用被代理类的同名方法
 */

class ProxyFactory {
    //调用此方法,返回一个代理类的对象。解决问题一
    public static Object getProxyInstance(Object obj) {//obj:被代理类的对象
		//1.创建一个实现接口 InvocationHandler的类,它必须实现invoke方法,以完成代理的具体操作。
        MyInvocationHandler handler = new MyInvocationHandler();
        handler.bind(obj);
		//3. 通过 Proxy 的静态方法newProxyInstance() 创建一个subject接口代理
        return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(),handler);
    }
}


class MyInvocationHandler implements InvocationHandler {

    private Object obj; //需要使用被代理类的对象进行赋值

    public void bind(Object obj) {
        this.obj = obj;
    }

    //当我们通过代理类的对象,调用方法时,就会自动的调用如下的方法:invoke()
    //将被代理类要执行的方法的功能就声明在invoke()中
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable 	{
        //proxy:代理类的对象
        //args: 方法调用时所需要的参数
        //method:即为代理类对象调用的方法,此方法也就作为了被代理类对象要调用的方法
        //obj:被代理类的对象
        Object returnValue = method.invoke(obj, args);
        return returnValue;
    }
}




public class proxyTest {
    public static void main(String[] args) {
        SuperMan superMan = new SuperMan();
        //这里返回的不是superMan,只是和它实现了同一个接口
        Human proxyInstance = (Human) ProxyFactory.getProxyInstance(superMan);

        System.out.println(proxyInstance.getBelief());
        proxyInstance.eat("辣辣辣!!!");

    }
}

AOP(Aspect Orient Programming)

public class MyInvocationHandler implements InvocationHandler {
    //需要被代理的对象
    private Object target;

    public void setTarget(Object target) {
        this.target = target;
    }

    //执行动态代理对象的所有方法时,都会被替换成执行如下的 invoke 方法
    public Object invoke(Object proxy, Method method, Object[] args) throws Exception 	{
        //这里是在目标方法之前加入了一些自己通用的处理方法
        DogUtil du = new DogUtil();
		//执行 DogUtil 对象中的 method1 。
        du.method1();
        
		//以 target 作为主调来执行 method 方法
        Object result = method.invoke(target, args);
        
		//执行 DogUtil 对象中的 method2 。
        du.method2();
        return result;
    }
}

在这里插入图片描述

AOP 代理可代替目标对象, AOP 代理包含了目标对象的全部方法。但 AOP 代理中的方法与目标对象的方法存在差异:AOP 代理里的方法可以在执行目标方法之前、之后插入一些通用处理


Lambda 表达式

Lambda是一个 匿名函数 ,我们可以把 Lambda 表达式理解为是 一段可以传递的代码 (将代码像数据一样进行传递)。使用它可以写出更简洁、更灵活的代码。作为一种更紧凑的代码风格,使 Java 的语言表达能力得到了提升。

  • 从匿名类到 Lambda 的转换举例(只能有一个抽象方法)
    在这里插入图片描述在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

操作符 → \rightarrow 即Lambda 操作符或 箭 头操作符 。它将 Lambda 分为两个部分:

  • 左侧:指定了 Lambda 表达式需要的参数列表
  • 右侧:指定了 Lambda 体是抽象方法的实现逻辑,也即Lambda 表达式要执行的功能 。

在这里插入图片描述
在这里插入图片描述

函数式 ( Functional)接口

只包含一个抽象方法的接口,称为函数式接口 (在 java.util.function包下)

  • @FunctionalInterface 注解,可以检查它是否是一个函数式接口。同时 javadoc 也会包含一条声明,说明这个接口是一个函数式接口。
  • 在java中,Lambda表达式是对象,而不是函数,它们必须依附于一类特别的对象类型 ——函数式接口Lambda 表达式就是一个函数式接口的实例
  • 所以以前用 匿名实现类 表示的现在都可以用 Lambda表达式来写。
    在这里插入图片描述

在这里插入图片描述

自定义函数式接口

在这里插入图片描述

函数式接口中使用泛型:

在这里插入图片描述

作为参数传递 Lambda 表达式

在这里插入图片描述

作为参数传递Lambda 表达式:为了将 Lambda 表达式作为参数传递,接收 Lambda表达式的参数类型必须是与该 Lambda 表达式兼容的函数式接口的类型。

********************* 以下方法引用部分没搞懂 ***************************

方法引用与构造器引用

方法引用(Method)

当要传递给 Lambda 体的操作,已经有实现的方法了,可以使用方法引用!(语法糖)

  • 要求:实现接口的抽象方法的参数列表和返回值类型,必须与方法引用的方法的参数列表和返回值类型保持一致!
  • 格式: 使用操作符 “::” 将类(或对象 ) 与方法名分隔开来。
  • 如下三种主要使用情况:
    • 对象 :: 实例方法名
    • 类 :: 静态方法名
    • 类 :: 实例方法名

在这里插入图片描述

等同于:
在这里插入图片描述

在这里插入图片描述

等同于:

在这里插入图片描述

在这里插入图片描述

注:当函数式接口方法的第一个参数是需要引用方法的调用者 ,并且第二个参数是需要引用方法的参数 或无参数 时: ClassName::methodName

构造器引用

  • 格式ClassName::new

可以把构造器引用赋值给定义的方法,要求构造器参数列表要与接口中抽象方法的参数列表一致!且方法的返回值即为构造器对应类的对象。

在这里插入图片描述

数组引用

  • 格式type[] :: new

在这里插入图片描述

Stream API

Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。 使用Stream API (java.util.stream)对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。也可以使用 Stream API 来并行执行操作。简言之, Stream API 提供了一种高效且易于使用的处理数据的方式

  • Stream 和 Collection 集合的区别Collection 是一种静态的内存数据结构,而 Stream 是有关计算的。 前者是主要面向内存,存储在内存中,后者主要是面向 CPU ,通过 CPU 实现计算。

注:①Stream 自己不会存储元素。
②Stream 不会改变源对象。相反,他们会返回一个持有结果的新 Stream 。
③Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。

Stream的操作三个步骤

  1. 创建 Stream
    一个数据源(如:集合、数组),获取一个流
  2. 中间操作
    一个中间操作链,对数据源的数据进行处理
  3. 终止操作 (终端操作)
    一旦执行终止操作, 就执行中间操作链 ,并产生结果 。之后,不会再被使用

在这里插入图片描述

创建Stream

  1. 通过集合

    Java8中的 Collection 接口被扩展,提供了两个获取流的方法

    • default Stream stream() : 返回一个顺序流

    • default Stream parallelStream() : 返回一个并行流

  2. 通过数组
    Java8中的 Arrays 的静态方法 stream() 可以获取数组流:

    • static Stream stream(T[] array): 返回一个流
  3. 通过 Stream 的 of()
    可以调用Stream 类静态方法 of(), 通过显示值创建一个流。它可以接收任意数量的参数。

    • public static Stream of(T… values) : 返回一个流
  4. 创建无限流

    可以使用静态方法Stream.iterate() 和 Stream.generate(),创建无限流。

    • 迭代
      public static Stream iterate(final T seed, final UnaryOperator f)
    • 生成
      public static Stream generate(Supplier s)
@Test
public void test4() {
    //迭代
    //seed表示初始值,UnaryOperator为函数式接口,limit()为终止条件
    Stream<Integer> stream = Stream.iterate(0, x--> x + 2);
    stream.limit(10).forEach(System.out::println);

    //生成
    Stream<Double> stream1 = Stream.generate(Math::random);
    stream1.limit(10).forEach(System.out::println);
}

Stream的中间操作多个

​ 多个中间操作可以连接起来形成一个流水线 ,除非流水线上触发终止操作,否则中间操作不会执行任何的处理 !而在 终止操作时一次性全部处理,称为“惰性求值” 。

在这里插入图片描述

/*
 * 一、Stream API 的操作步骤:
 * 
 * 1. 创建 Stream
 * 
 * 2. 中间操作
 * 
 * 3. 终止操作(终端操作)
 */
public class TestStreamAPI {
    
    //1. 创建 Stream
    @Test
    public void test1(){
        //1. Collection 提供了两个方法  stream() 与 parallelStream()
        List<String> list = new ArrayList<>();
        Stream<String> stream = list.stream(); //获取一个顺序流
        Stream<String> parallelStream = list.parallelStream(); //获取一个并行流
        
        //2. 通过 Arrays 中的 stream() 获取一个数组流
        Integer[] nums = new Integer[10];
        Stream<Integer> stream1 = Arrays.stream(nums);
        
        //3. 通过 Stream 类中静态方法 of()
        Stream<Integer> stream2 = Stream.of(1,2,3,4,5,6);
        
        //4. 创建无限流
        //迭代
        Stream<Integer> stream3 = Stream.iterate(0, (x) -> x + 2).limit(10);
        stream3.forEach(System.out::println);
        
        //生成
        Stream<Double> stream4 = Stream.generate(Math::random).limit(2);
        stream4.forEach(System.out::println);
    }
    
    //2. 中间操作
    List<Employee> emps = Arrays.asList(
            new Employee(102, "李四", 59, 6666.66),
            new Employee(101, "张三", 18, 9999.99),
            new Employee(103, "王五", 28, 3333.33),
            new Employee(104, "赵六", 8, 7777.77),
            new Employee(104, "赵六", 8, 7777.77),
            new Employee(104, "赵六", 8, 7777.77),
            new Employee(105, "田七", 38, 5555.55)
    );
    
    /*
      筛选与切片
        filter——接收 Lambda , 从流中排除某些元素。
        limit——截断流,使其元素不超过给定数量。
        skip(n) —— 跳过元素,返回一个扔掉了前 n 个元素的流。若流中元素不足 n 个,则返回一个空流。与 limit(n) 互补
        distinct——筛选,通过流所生成元素的 hashCode() 和 equals() 去除重复元素
     */
    
    //内部迭代:迭代操作 Stream API 内部完成
    @Test
    public void test2(){
        //所有的中间操作不会做任何的处理
        Stream<Employee> stream = emps.stream()
            .filter((e) -> {
                System.out.println("测试中间操作");
                return e.getAge() <= 35;
            });
        
        //只有当做终止操作时,所有的中间操作会一次性的全部执行,称为“惰性求值”
        stream.forEach(System.out::println);
    }
    
    //外部迭代
    @Test
    public void test3(){
        Iterator<Employee> it = emps.iterator();
        
        while(it.hasNext()){
            System.out.println(it.next());
        }
    }
    
    @Test
    public void test4(){
        emps.stream()
            .filter((e) -> {
                System.out.println("短路!"); // &&  ||
                return e.getSalary() >= 5000;
            }).limit(3)
            .forEach(System.out::println);
    }
    
    @Test
    public void test5(){
        emps.parallelStream()
            .filter((e) -> e.getSalary() >= 5000)
            .skip(2) //跳过前两条数据
            .forEach(System.out::println);
    }
    
    @Test
    public void test6(){
        emps.stream()
            .distinct() //去重
            .forEach(System.out::println);
    }
}

在这里插入图片描述

@Test
 public void test7() {
     List<String> list = Arrays.asList("aaa", "java", "ccc", "java8", "hello world");

     list.stream()
             .map((x) -> x.toUpperCase())
             .forEach(System.out::println);

     System.out.println("-------------");

     emps.stream()
             .map(Employee::getAge)
             .forEach(System.out::println);
 }

在这里插入图片描述

Stream的终止操作

终端操作会从流的流水线生成结果。其结果可以是任何 不是流的值,例如: List 、 Integer ,甚至是 void 。流进行了终止操作后,不能再次使用。

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

Optional 类

Optional 类 (java.util.Optional) 是一个容器类,它可以保存类型 T 的值, 代表这个值存在 。或者仅仅保存 null ,表示这个值不存在 。原来 用 null 表示一个值不存在,现在 Optional 可以更好的表达这个概念。并且 可以避免空指针异常 。

  • Optional 类的 Javadoc 描述如下:这是一个可以为 null 的容器对象。如果值存在则 isPresent() 方法会返回 true ,调用 get() 方法会返回该对象。

  • 创建 Optional 类对象的方法:

    • Optional.of(T t) : 创建一个 Optional 实例, t 必须非空
    • Optional.empty() : 创建一个空的 Optional 实例
    • Optional.ofNullable(T t) t 可以为 null
  • 判断 Optional 容器中是否包含对象:

    • boolean isPresent() : 判断是否包含对象
    • void ifPresent(Consumer<? super T> consumer) 如果有值,就执行 Consumer接口的实现代码,并且该值会作为参数传给它。
  • 获取 Optional 容器的对象:

    • T get(): 如果调用对象包含值,返回该值,否则抛异常
    • T orElse(T other) 如果有值则将其返回,否则返回指定的 other 对象。
    • T orElseGet(Supplier<? extends T> other) 如果有值则将其返回,否则返回由Supplier 接口实现提供的对象。
    • T orElseThrow(Supplier<? extends X> exceptionSupplier) 如果有值则将其返
      回,否则抛出由 Supplier 接口实现提供的异常 。
@Test
public void test1() {
    Boy b = new Boy( "张三");
    Optional<Girl> opt = Optional.ofNullable(b.getGrilFriend());;
    //如果女朋友存在就打印女朋友的信息
    opt.ifPresent(System.out::println)}
@Test
public void test2() {
    Boy b = new Boy( "张三");
    Optional<Girl> opt = ofNullable(b.getGrilFriend());
    //如果有女朋友就返回他的女朋友,否则只能欣赏“嫦娥”了
    Girl girl = opt.new Girl("嫦娥");
    System.out.println("他的女朋友是:"+ girl.getName());
}

知识的搬运工,搬了太多大佬的东西了,忘记都是哪的了,那就感谢各位佬们吧!!!

  • 0
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值