JAVA 多线程,常用类,泛型 精析

Java高级

多线程

module:模块

一个功能用一个模块(分布式部署)

IDEA中 project 是最高级的,其次是 module

module 可以 remove(或者打开setting)

基本概念

程序(program):指令的集合(静态的代码)

进程(process):正在运行的一个程序。动态过程 —生命周期

  • 程序是静态的,进程是动态的
  • 进程为资源分配的单位,系统会为每个进程分配不同的内存区域(注意不是线程,是进程)

线程(thread):进程可以细化为线程,是程序内部的一条执行路径

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

单核CPU和多核CPU的理解

单核CPU是假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。

如果是多核的话,才能更好的发挥多线程效率。(现在服务器都是多核的)

一个java应用程序,至少有三个线程:主线程、gc()垃圾回收线程、异常处理线程。当然如果发生异常,会影响主线程

并发和并行

并发:一个CPU(采用时间片)同时执行多个任务。比如:多个人做一件事

并行:多个CPU同时执行多个任务。比如:多个人做不同的事

多线程的优点

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

什么时候需要多线程

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

创建多线程

继承Thread类
  • 每个线程都是通过特定 Thread 对象的 run() 方法来完成操作的,经常把 run() 方法的主体称为线程体
  • 通过该 Thread 对象的 start() 方法来启动这个线程

执行次序取决于线程的调度(不一定嗷)

public class Main {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();
        t.run();//不是多线程嗷
    }
}


class MyThread extends Thread {
    static int ans = 0;
    public void run() {
        for(int i = 0; ; i++){
            ans += 1;
            if(ans == ((1 << 31) - 1)) {
                System.out.println(ans);
                break;
            }
        }
    }
}

调用 run() 是不行的,虽然可以执行,但是不是多线程

Thread.currentThread().getName() 可以查看当前线程名字

多个线程造多个对象

线程常用方法

start():启动当前的线程,调用当前线程的run()
run():通常需要重写Thread类中的此方法,将创建要执行的操作声明在此方法中
currentThread():静态方法,返回代码执行的线程
getName():获取当前线程的名字
setName():设置当前线程的名字 (构造器也可以设置(声明时))
yield():释放当前CPU的执行权(直接用,省略的是this.)
join():在线程a中调用线程b的join()方法,此时线程a进入阻塞状态,b执行完后a才解除阻塞
sleep(long millitime):让当前进程睡眠指定的毫秒数,在指定时间内,线程是阻塞状态
isAlive():判断进程是否存活

线程的调度

抢占式:高优先级的线程抢占cpu

同优先级线程组成先进先出队列

高优先级优先调度

高优先级线程有更大概率获得调度?

优先级常量
  1. MAX_PRIORITY 10
  2. MIN_PRIORITY 1
  3. NORM_PRIORITY 5
涉及的方法

getPriority(); 返回线程优先值

setPriority(set newPriority) : 改变线程优先级

说明:

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

低优先级只是被调度的概率低,并不是等高优先级调度完后才被调用

实现Runable 来创建多线程

  1. 创建实现了Runable 接口的类
  2. 实现类去实现 Runable 中的抽象方法:run() 方法
  3. 创建实现类的对象
  4. 创建Thread 类的对象,将此对象用作参数传递到Thread 类的构造器中
  5. 通过Thread 类的对象调用start()
public class Main {
    public static void main(String[] args) {
        //调用
        MyThread2 t2 = new MyThread2();
        Thread t22 = new Thread(t2);
        // 启动线程 -> 调用当前线程的 run() -> 调用Runnable类型的 target(此处为 t2) 的run()
        t22.start();
    }
}
class MyThread2 implements Runnable{
    static int ans = 0;
    @Override
    public void run() {
        for(int i = 0; ; i++){
            ans += 1;
            if(ans == ((1 << 31) - 1)) {
                System.out.println(ans);
                break;
            }
        }
    }
}

可查看源码了解为什么

Runable 的参数可以用在多个线程的构造器中

匿名类:

new Thread(new Runnable() {
            @Override
            public void run() {
                
            }
        }).start();

两种方式的对比与联系

开发中优先使用Runnable 方式

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

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

  3. 继承Thread 后,该类不能再继承其他类,如果该类本身是继承了其他类的话,那么无法调用多线程

  4. Runnable 可以作为多个Thread 的参数,可以直接共享 Runnable 的数据

联系:
Thread 也实现了Runnable 接口

线程的生命周期

JDK 中用Thread.State 类定义了线程的几种状态

要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:

新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态

就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它己具备了运行的条件,只是没分配到CPU资源

运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能

阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态

死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束

如何中断一个线程

1)使用退出标志

public static volatile boolean exit =false;  //退出标志
    public static void main(String[] args) {
        new Thread() {
            public void run() {
                System.out.println("线程启动了");
                while (!exit) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("线程结束了");
            }
        }.start();
        
        try {
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        exit = true;//5秒后更改退出标志的值,没有这段代码,线程就一直不能停止
    }

2)使用 interrupt 方法

​ Thread.interrupt()方法: 作用是中断线程。将会设置该线程的中断状态位,即设置为true,中断的结果线程是死亡、还是等待新的任务或是继续运行至下一步,就取决于这个程序本身。线程会不时地检测这个中断标示位,以判断线程是否应该被中断(中断标示值是否为true)。它并不像stop方法那样会中断一个正在运行的线程

​ interrupt()方法只是**改变中断状态,不会中断一个正在运行的线程。需要用户自己去监视线程的状态为并做处理。**支持线程中断的方法(也就是线程中断后会抛interruptedException的方法)就是监视线0

程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。这一方法实际完成的是,给受阻塞的线程发出一个中断信号,这样受阻线程检查到中断标识,就得以退出阻塞的状态

​ 更确切的说,如果线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞,此时调用该线程的interrupt()方法,那么该线程将抛出一个InterruptedException中断异常(该线程必须事先预备好处理此异常),从而提早地终结被阻塞状态。如果线程没有被阻塞,这时调用 interrupt()将不起作用直到执行到wait(),sleep(),join()时,才马上会抛出 InterruptedException。

public static void main(String[] args) {
        Thread thread = new Thread() {
            public void run() {
                System.out.println("线程启动了");
                try {
                    Thread.sleep(1000 * 100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程结束了");
            }
        };
        thread.start();

        try {
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread.interrupt();//作用是:在线程阻塞时抛出一个中断信号,这样线程就得以退出阻塞的状态
    }

3)使用 interrupt() + isInterrupted()来中断线程

this.interrupted():测试当前线程是否已经中断(静态方法)。如果连续调用该方法,则第二次调用将返回false。在api文档中说明interrupted()方法具有清除状态的功能。执行后具有将状态标识清除为false的功能。

**this.isInterrupted()😗*测试线程是否已经中断,但是不能清除状态标识。

public static void main(String[] args) {
        Thread thread = new Thread() {
            public void run() {
                System.out.println("线程启动了");
                while (!isInterrupted()) {
                    System.out.println(isInterrupted());//调用 interrupt 之后为true
                }
                System.out.println("线程结束了");
            }
        };
        thread.start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread.interrupt();
        System.out.println("线程是否被中断:" + thread.isInterrupted());//true
    }

关于interrupted()和isInterrupted()方法的注意事项说明

interrupted()是静态方法:内部实现是调用的当前线程的isInterrupted(),并且会重置当前线程的中断状态。

测试当前线程是否已经中断(静态方法)。返回的是上一次的中断状态,并且会清除该状态,所以连续调用两次,第一次返回true,第二次返回false。

isInterrupted()是实例方法,是调用该方法的对象所表示的那个线程的isInterrupted(),不会重置当前线程的中断状态测试线程当前是否已经中断,但是不能清除状态标识。

状态的变化

start() 后不是运行状态而是就绪状态

失去cpu执行权或yield() 会从运行状态到就绪状态

执行完调用线程的stop() 或者出现Error/Exception然后线程死亡

运行状态到阻塞状态,阻塞不是最终状态,必须先进入就绪状态才能运行(sleep(long time) | join() | 等待同步锁 | wait() | suspend() - >线程挂起,过时方法,可能引起线程死锁)

阻塞到就绪(sleep() 时间到、join() 结束、获取了同步锁、notify() / notifyAll() 、resume())

线程的同步(安全问题)

例如:取钱问题

  • 多个线程执行的不确定性引起执行结果的不稳定

  • 多个线程对账本的共享,会造成操作的不完整性,会破坏数据

解决:当一个线程再操作时,其他线程不能参与进来。知道操作完成,其他线程才可以参与进来(这种情况即使线程1出现了阻塞,也不能被改变)

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

synchronized

法一:同步代码块

synchronized(obj){
//需要同步的代码
}
class MyThread extends Thread {
    static int ans = 0;
    @Override
    public void run() {
        synchronized (MyThread2.class) {
            for (int i = 0; ; i++) {
                ans += 1;
                if (ans == ((1 << 31) - 1)) {
                    System.out.println(ans);
                    break;
                }
            }
        }

    }
}

说明:操作共享数据(多个线程共享的变量)的代码,即为需要被同步的代码(不能包多了(逻辑出错)也不能包少了)

同步监视器俗称:锁

任何一个类的对象都可以充当锁(看上文的锁的参数obj),要求:多个线程必须共用同一把锁

synchronized(MyThread2.class{ // 这个类是唯一的,因为类只会加载一次
//需要同步的代码
}

这个类是唯一的,因为类只会加载一次

如果实现的是Runnable 可以考虑用this 当同步监视器

可以把锁写在Runnable 的实现类的成员中

法二:同步方法

如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的,

方法声明时加 synchronized : public synchronized void Op(){};

这样的话同步监视器是this

在面对继承于Thread 的类时,锁就有很多个了(只能考虑把方法写成静态的,只会加载一次 | 也就相当于MyThread2.class)

  • 同步方法仍然有同步监视器。只不过不需要显式声明
  • 非静态同步方法:this
  • 静态同步方法:当前类本身

同步的过程:相当于只能有一个线程参与,其他线程等待。相当于是单线程的过程,效率低

懒汉式单例模式的修改(线程安全)

class DogFather { // 线程安全
    private DogFather() {
    }

    private static DogFather instance = null;

    public static DogFather getInstance() {
        //如果不加判断,没调用一次方法就会new新的对象
        if(instance != null){
            return instance;
        }
        synchronized(DogFather.class){
            if (instance == null) {
                instance = new DogFather();
            }
        }
        return instance;
    }
}

Lock(锁)

JDK5.0 开始可以通过显式定义同步锁对象来实现同步。 同步锁使用Lock对象来充当。

java.util.concurrent.locks.Lock 接口时控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源前应获得Lock对象

ReentrantLock 类实现了Lock,它拥有与synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显示加锁、释放锁

注意:

如果是实现 Runnable 接口,则可以

private ReentrantLock lock = new ReentrantLock();

如果是继承于Thread 类,则需要声明为静态

public void run() {
        while (true) {
            lock.lock(); //调用锁定方法lock()
            try{
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if(ticket > 0){
                    System.out.println(Thread.currentThread().getName() + ticket--);
                }else {
                    break;
                }
            }finally {
                lock.unlock();//解锁
            }

        }
    }
synchronized 和 Lock 异同:

同:

  • 都可以解决线程安全问题

异:

  • synchronized 机制在执行完相应的同步代码后,自动释放同步监视器。Lock 需要手动的启动 lock() ,同时结束也需要手动实现 unlock()

在开发中一般用Lock,手动的更加灵活

优先顺序:

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

三种方法解决线程安全问题

线程死锁问题

  • 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,形成了线程的死锁
  • 出现死锁问题后不会有异常、提示,只是线程处于阻塞状态,无法继续

解决方法:

  • 专门的算法、原则
  • 尽量减少同步资源的定义
  • 尽量避免嵌套同步

线程的通信

例题:使用两个线程打印1 - 100.线程1、线程2 交替打印

wait() 可以让调用它的线程进入阻塞状态,并且如果有锁,会释放锁

notify() 可以唤醒wait() 的一个线程,如果多个线程处于阻塞,唤醒优先级高的 (notifyAll() 唤醒所有线程)

synchronized(this){
    wait();
}

省略的是 **this.**wait();

说明:

  1. 三个方法必须使用在同步代码块或同步方法中
  2. 三个方法调用者必须是同步代码块或同步代码方法的同步监视器否则会出现异常
  3. 三个方法定义在 java.Lang.Object类中,因为任何一个类都可以当同步监视器

所以例题可以在打印一个后调用wait() 然后锁被释放,第二个线程拿着锁刚进入时,就唤醒另一个线程,然后交替进行

面试题

1.sleep() 方法和wait() 方法的异同?

同:

执行了方法后,都进入阻塞状态

异:

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

2)调用的要求不同:sleep() 可以在任何需要的场景下使用,wait() 必须在同步代码块或同步方法调用,因为需要同步监视器调用

3)关于是否释放锁:sleep() 不会释放锁,wait() 会

2.创建多线程有4种方式

通信的应用例题

视频号 441

JDK5.0 新增线程创建方式

实现Callable 接口

和实现 Runnable 相比,实现Callable 功能更强大

  • 相比run() 方法,可以有返回值
  • 方法可以抛出异常
  • 支持泛型的返回值
  • 需要借助FutureTask类,比如获取返回结果

Future 接口

  • 可以对具体Runnable、Callable 任务的执行结果进行取消、查询是否完成、获取结果等
  • FutureTask 是Future 接口的唯一实现类
  • FutureTask 同时实现了Runnable,Future接口,它既可以作为Runnable 被线程执行,又可以作为Future 得到Callable 的返回值
  1. 实现 Callable 类
  2. 重写 call方法
  3. 创建 Callable 的对象
  4. 把对象最为参数传到 FutureTask 的构造器中,创建 FutureTask 对象
  5. 获取Callable 中call 方法的返回值 ——get() 方法
public class Main3 {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        NumThread nt = new NumThread();

        FutureTask<Integer> a = new FutureTask<>(nt);
        new Thread(a).start();

        try {
            Integer sum = a.get();  //自动调用 call 方法, 返回值是 FutureTask 构造器参数的重写的 call() 的返回值
            System.out.println(sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        in.close();
    }
}
class NumThread implements Callable {

    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for(int i = 1;  i <= 100; i++) {
            if(i % 2 == 0) {
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}
使用线程池

背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。

思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中,可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具

好处:

  • 提高相应速度(减少了创建新线程的时间)
  • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
  • 便于线程管理
    • corePoolSize 核心池大小
    • maximumPoolSize 最大线程数
    • keepAliveTime 线程没有任务时最多保持多长时间会终止

ExecutorService 和 Executors

ExecutorService :真正的线程池接口。常见子类:ThreadPoolExecutor

public class Main {
    public static void main(String[] args) {
        //右边返回的是一个接口。多态,返回的是接口实现类的对象
        ExecutorService service = Executors.newFixedThreadPool(10);
        //设置线程池的对象
        System.out.println(service.getClass());
        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
        service1.setCorePoolSize(15);
    //    service1.setKeepAliveTime();

        //执行指定的线程的操作,需要提供Runnable 或Callable 接口实现类的对象
        service.execute(new NumThread()); //适合使用 Runnable
        service.execute(new NumThread1());
  //      service.submit(Callable callable); // 适合适用于 Callable,此方法有返回值
        service.shutdown();
    }
}
class NumThread implements Runnable {
    @Override
    public void run() {
        for(int i = 0; i <= 100; i++) {
            if(i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}
class NumThread1 implements Runnable {
    @Override
    public void run() {
        for(int i = 0; i <= 100; i++) {
            if(i % 2 != 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

守护线程

setDaemon方法必须在start方法前定义。

setDaemon(true)方法是把java的线程设置为守护线程。
在Java中有两dao类线程:User Thread(用户线程)、Daemon Thread(守护线程)
守护线程就是运行在程序后台的线程,通俗的比如,任何一个守护线程都是整个JVM中所有非守护线程的保姆。
Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器),就是一个很称职的守护者。User和Daemon两者几乎没有区别,唯一的不同之处就在于虚拟机的离开如果 User Thread已经全部退出运行了,只剩下Daemon Thread存在了,虚拟机也就退出了。 因为没有了被守护者,Daemon也就没有工作可做了,也b就没有继续运行程序的必要了。

Java 常用类

字符串相关类 String StringBuffer StringBuilder

JDK 8之前的日期时间API

JDK 8中新日期时间API

Java 比较器 Comparable接口 Comparator接口 比较引用数据类型

System类

Math类

BigInterger与BigDecimal

String 类

对象的字符内容是存在final char value[] 中的

String 的实现了Serializeable 接口:表示字符串是支持序列化的(网络编程)

实现了 Comparable 接口 可以比较大小

字符串常量池:

String s1 = "123";
String s2 = "123";
s1 = "1234";
String s3 = "123";
s3 += "45";

s1,s2的地址相同,字符串常量池不会有相同的字符常量

此时常量池有两个常量

s3的操作又让常量池新增一个常量

此时常量池有三个常量

String.replace(x, y) // 把x换成y

String不同实例化方式的对比
String str = "123";
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[] a, int startIndex, int count);
等等...

区别 String str1 = “123”; String str2 = new String(“123”);

一个在字符串常量池,一个在堆中

new两个,内容相同,== 比较的是地址

注意:

Person p1 = new Person("name", 12);
Person p2 = new Person("name", 12);
System.out.println(p1.name = p2.name);//ture

因为name 是用String 实现,所以他们都在常量池中

面试题:

String s = new String("abc"); 创建了几个对象?

答案:两个,一个在堆中,另一个char[] 对应的常量池中的数据:"abc"

String类型的拼接

String s2 = s1.intern(); 返回s1的String类型,并且是常量池的String

拼接就是用 +

注意:

  • 常量和常量的拼接在常量池

  • 只要拼接的一方是变量,结果就在

  • intern()方法,返回值就在常量中

    final String s1 = "123"; // s1是常量,所以和常量相加结果在常量池
    
面试题(String 值传递):
public class Main {
    String str = new String("good");
    char[] ch = {'t', 'e', 's', 't'};

    public static void main(String[] args) {
        Main a = new Main();
        a.change(a.str, a.ch);
        System.out.print(a.str + "and");
        System.out.println(a.ch);
    }

    public void change(String str, char ch[]) {
        str = "shit";
        ch[0] = 'b';
    }
}

输出为goodandbest

public class Main {
    String str = new String("good");
    char[] ch = {'t', 'e', 's', 't'};

    public static void main(String[] args) {
        Main a = new Main();
        a.change(a.str, a.ch);
        System.out.print(a.str + "and");
        System.out.println(a.ch);
    }

    public void change(String str, char ch[]) {
        this.str = "shit";
        ch[0] = 'b';
    }
}

值改了

注意str 是形参,如果去掉this,那么类的str还是没变

JVM String的内存解析

JVM虚拟机有多种

一般来说是HotSpot

堆 Heap

一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,方便执行器执行。堆内存分为三部分:

  • 新生代 Young Generation Space Young
  • 养老代 Tenure Generation Space Old
  • 永久存储区 Permanent Space Perm //实际上是方法区(Non-Heap)

从规范说是三部分,实际是当成两部分看

String s = new String(“12300”);

以上代码中的 s 指向了堆中的地址,但是 s 的 value 仍然是常量池的内容

String 常用方法

  • equals:字符串是否相同
  • equalsIgnoreCase:忽略大小写后字符串是否相同
  • compareTo:根据字符串中每个字符的Unicode编码进行比较 (负数,当前小,正数,当前大)
  • compareToIgnoreCase:根据字符串中每个字符的Unicode编码进行忽略大小写比较
  • int indexOf(String str):目标字符或字符串在源字符串中位置下标
  • int lastIndexOf(String str):目标字符或字符串在源字符串中最后一次出现的位置下标
  • valueOf():其他类型转字符串
  • char charAt(int index):获取指定下标位置的字符
  • codePointAt:指定下标的字符的Unicode编码
  • concat:追加字符串到当前字符串(原来的String 不变,返回追加后的String) 类似 ’ + ’
  • boolean isEmpty():字符串长度是否为0
  • contains:是否包含目标字符串
  • startsWith:是否以目标字符串开头
  • endsWith:是否以目标字符串结束
  • format:格式化字符串
  • getBytes:获取字符串的字节数组
  • getChars:获取字符串的指定长度字符数组
  • toCharArray:获取字符串的字符数组
  • length():字符串字符数
  • matches:字符串是否匹配正则表达式 如:判断是否为电话号码、身份证号码
  • replace:字符串替换
  • replaceAll:带正则字符串替换
  • replaceFirst:替换第一个出现的目标字符串
  • split:以某正则表达式分割字符串
  • **String substring(int beginIndex)、String substring(int beginIndex, int endIndex)截取字符串 ** 注意后者左闭右开!
  • String toLowerCase():字符串转小写
  • String toUpperCase():字符串转大写
  • String trim():去字符串首尾空格

split 前期可以用来分割:

String s1 = "123 b456 789";
        String ss[] = s1.split(" ");
        for(String i : ss) {
            System.out.println(i);
        }

字符串反转

String s1 = "123";
StringBuilder s2 = new StringBuilder(s1);
System.out.println(s2.reverse().toString());
String s1 = "123";
StringBuffer s2 = new StringBuffer(s1);
System.out.println(s2.reverse().toString());
String 和 基本类型/包 /char[] 的转换
String 和 基本 的相互转换
String str = "123";
int num = Integer.parseInt(str);
String str = String.valueOf(num);
char[] 和 String 的相互转换
String str = "123";
char ch[] = str.toCharArray();
String str2 = new String(ch);
byte[] 和 String 的相互转换

IO流会涉及

String s1 = "123中国";
byte[] bytes = s1.getBytes(); // 使用默认的字符集进行转换
System.out.println(Arrays.toString(bytes)); //看下输出 注意编码不是UTF-8

byte[] gbks  = s1.getBytes("gbk"); //gbk 编码
System.out.println(Arrays.toString(gbks));

String s2 = new String(bytes); //默认字符集解码

String s3 = new String(gbks);//乱码 编码集和解码集不同
s3 = new String(gbks, "gbk");

编码:看得懂的 -> 字节

String -> byte[]

解码:字节 -> 看得懂的

byte[] -> String

web 中会涉及,以后会涉及

StringBuffer 和 StringBuilder

和String区别:

  • StringBuffer
    • 可变
    • 线程安全,效率低
  • StringBuilder (JDK5.0新增)
    • 可变
    • 线程安全,效率高一些
StringBuffer sb = new StringBuffer("abc");
sb.setCharAt(0, 'm'); // sb存的是 "mbc"

StringBuffer 、StringBuilder

三者同:

  • 底层用char[] 存储

源码分析:

String str = new String();//char[] value = new char[0];
String str1 = new String("abc");//char[] value = new char[] = ('a', 'b', 'c');
StringBuffer sb1 = new StringBuffer();//char[] value = new char[16];底层创建了一个长度16的数组
StringBuffer sb2 = new StringBuffer( "abc");//char[] value = new char[ "abc ".length() + 16]

扩容问题:

如果添加的数据底层数组存不下,默认扩容到原来的2倍 + 2,如果还存不下,就扩到需要的大小


如果是多线程程序,那么使用StringBuffer 处理,反之用StringBuilder

效率对比

如果字符串需要频繁修改,那么推荐用StringBuffer

String 最慢!

推荐用 StringBuffer(int capacity)

常用方法
增:append();
删:StringBuffer delete(int start, int end); // 删除指定位置,涉及开始结束的,都是左闭右开 如:subString(int start, int end);
修改:StringBuffer replace(int start, int end, String str); //把[start, end) 的位置替换为str
setCharAt(int n, char ch);
查:charAt(int n);
插入:insert(int offset, xxx); // 指定位置插入xxx、
遍历:for + charAt()
长度:length()
reverse(); 把当前字符序列逆转

append 和 insert 时如果数组长度不够,会扩容

如上方法支持方法链

public StringBuilder append(String str) {
    super.append(str);
    return this;
}

返回的this 还是对象本身

注意关注源码

String str = null;
StringBuffer sb = new StringBuffer();
sb.append(str);
System.out.println(sb.length()); // 4
System.out.println(sb); // null  这个null 是字符串的"null"

// StringBuffer sb2 = new StringBuffer(str); // 抛异常空指针
// System.out.println(sb2);

日期时间的API

Jdk8 前的日期时间API
  1. java.lang.System 类
    public static long curentTimeMillis() 用于返回当前时间与1970年1月1日0时0分0秒之间以毫秒为单位的时间差

    此方法用于计算时间差

  2. java.util.Date (java.sql.Date)类 并且两个类是子父类关系

    两个构造器的使用,两个方法的使用

    Date date1 = new Date(); //创建一个对应当前时间的Date对象
    System.out.println(date1.toString()); // 时间
    System.out.println(date1.getTime()); // 毫秒数
    
    Date date2 = new Date(date1.getTime());
    System.out.println(date2.toString());
    
  3. java.sql.Date 类 数据库里的Date 对应java的这个Date。和数据库交互会涉及

    • 如何实例化

    • 如何进行转换

      java.sql.Date date3  = new java.sql.Date(2315153125512L);
      System.out.println(date2);
      // 转换报错!!
      Date date4 = new Date();
      java.sql.Date date7 = (java.sql.Date) date4;
      // 正确转换方式!!
      Date date5 = new Date();
      java.sql.Date date6 = new java.sql.Date(date5.getTime());
      
SimpleDateFormat 的使用

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

它允许进行格式化:日期 -> 文本(字符串) 解析:文本 -> 日期

对 Date 类的格式化和解析

格式化和解析::

SimpleDateFormat sdf = new SimpleDateFormat();
// 日期 -> 字符串 格式化
Date date = new Date();
System.out.println(date);

String format = sdf.format(date);
System.out.println(format);

// 解析:
String str = "20-12-21 晚上23:00"; //格式得是这样的
Date date1 = sdf.parse(str);
System.out.println(date1);

注意格式化的字符串的样子,因为使用了默认的构造器

所以我们自己指定格式

SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); // 查找文档学会使用方式
String format1 = sdf2.format(date);
System.out.println(format1);

//解析,要求字符串是符合SimpleDateFormat 识别的格式,否则报错
Date date3 = sdf2.parse(:"2020-12-21 23:06:27");
System.out.println(date3);

例题:

  1. 字符串"2020-12-21" 转化为java.sql.Date
String birth = "2020-12-21";
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date date = sdf.parse(birth);
java.sql.Date birthDate = new java.sql.Date(date.getTime());
System.out.println(birthDate);
Clalendar 类

是一个抽象基类,主要用于完成日期字段之间相互操作的功能。

注意获取月份时:一月是0,十二月是11

注意获取星期:周日是1,周一是2,周六是7

使用:

Calendar c = Calendar.getinstance(); // 获得的是 Calendar 的子类
GregorianCalendar a = new GregorianCalendar(); // 和上面的一样

//常用方法 get() set() getTime() setTime()
int days = c.get(Calendar.DAY_OF_MONTH); // 一个月的第几天
System.out.println(days);

c.set(DAY_OF_MONH, 22);
days = c.get(Calendar.DAY_OF_MONTH); // 修改后
System.out.println(days);

c.add(Calendar.DAY_OF_MONTH, 3); // 加,可以加负数

Date date = c.getTime(); //返回Date

Date date1 = new Date();
c.setTime(date1);
days = c.get(Calendar.DAT_OF_MONTH);

JDK8 中的新日期时间API

Date类很少用了,许多方法被废弃,被Calendar 类代替

Calendar 面临的问题:

  • 可变性:像日期和时间这样的类应该是不可变的
  • 偏移性:Date的年份从1900开始,而月份都从0开始
  • 格式化:格式化只对Date有用,Calendar不行
  • 此外他们不是线程安全的:不能处理闰秒等
Date date1 = new Date(2020, 9, 22);
System.out.println(date1);// Fri Oct 22 00:00:00 CST 3920

Date date2 = new Date(2020 - 1900, 9 - 1, 22);
System.out.println(date2);

注意输出的月份和 9 不同,这就是偏移量的问题 可查看源码


java.time API

Joda - Time

LocalDate、LocalTime、LocaDateTime 的使用

        LocalDate localdate = LocalDate.now();
        LocalTime localtime = LocalTime.now();

        LocalDateTime localdatetime = LocalDateTime.now();//使用频率高
        //设定指定的 没有偏移量
        LocalDateTime localdatetime2 = LocalDateTime.of(2020,12,22,18,51,50);


        System.out.println(localdate); //2020-12-22
        System.out.println(localtime); //18:49:31.850
        System.out.println(localdatetime); //2020-12-22T18:49:31.850
        System.out.println(localdatetime2); //2020-12-22T18:51:50

        //get()
        System.out.println(localdatetime.getDayOfMonth()); // 22
        System.out.println(localdatetime.getDayOfWeek()); // TUESDAY

        System.out.println(localdatetime.getMonth()); // DECEMBER
        System.out.println(localdatetime.getMonthValue()); // 12

        //修改 (体现了不可变性)
        LocalDate localdate1 = localdate.withDayOfMonth(18);
        System.out.println(localdate); // 2020-12-22
        System.out.println(localdate1); // 2020-12-18


        //加月份
        LocalDateTime localdatetime3 = localdatetime.plusMonths(3);
        System.out.println(localdatetime); //2020-12-22T18:59:40.224
        System.out.println(localdatetime3); //2021-03-22T18:59:40.224

        //减 不是单纯变成负号
        localdatetime3 = localdatetime.minusMonths(3);
        System.out.println(localdatetime); // 2020-12-22T19:01:14.352
        System.out.println(localdatetime3); // 2020-09-22T19:01:14.352

**LocalDateTime 相对使用频率高。**类似于Calendar。


instant 瞬时类

instant:时间线的一个瞬时点。可能被用来记录应用程序事件时间戳

        //
        Instant instant = Instant.now(); // 获取本初子午线的时间
        System.out.println(instant); //2020-12-22T11:06:33.446Z 少了八小时

        //
        OffsetDateTime ofdt = instant.atOffset(ZoneOffset.ofHours(8)); // 2020-12-22T19:13:21.546+08:00
        System.out.println(ofdt);

        // 获取自1970-01-01 0.0.0 (UTC) 开始的毫秒数
        long ll = instant.toEpochMilli();
        System.out.println(ll);

        //实例化通过给定的毫秒数
        Instant instant1 = Instant.ofEpochMilli(ll);
        System.out.println(instant1);

类似于 java.util.Date 类


DateTimeFormatter

格式化与解析日期或时间

        //方式一:预定义的标准格式:如:IOS_LOCAL_DATE_TIME; ISO_LOCAL_DATE;ISO_LOCAL_TIME
        DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
        //格式化
        LocalDateTime ldt = LocalDateTime.now();
        String s = formatter.format(ldt);
        System.out.println(ldt);
        System.out.println(s); // 两个值相同,但是类型相同

        //解析
        TemporalAccessor parse = formatter.parse("2020-12-22T19:31:17.313");// 只能这种格式
        System.out.println(parse);

        //方式二:本地相关的格式化。 如:ofLocalizedDateTime(FormatStyle.LONG)
        // ofLocalizedDateTime()
        // FormatStyle.LONG / FormatStyle.MEDIUM / FormatStyle.SHORT :适用于LocalDateTime

        // ofLocalizedDate()
        // FormatStyle.FULL / FormatStyle.LONG / FormatStyle.MEDIUM / FormatStyle.SHORT :适用于LocalDate

        DateTimeFormatter formatter1 = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT);
        //格式化
        String format = formatter1.format(localdatetime);
        System.out.println(format); //20-12-22 下午7:37  SHORT 长这样,可以尝试不同的参数
        //解析同上

        DateTimeFormatter formatter2 = DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL);
        String format1 = formatter2.format(LocalDate.now());
        System.out.println(format1); // 2020年12月22日 星期二 长这样

        //重点:方式三:一般选择自定义如:ofPattern("yyyy-MM-dd hh:mm:ss") 像SimpleDateFormat 一样
        DateTimeFormatter formatter3 = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");
        String format2 = formatter3.format(LocalDateTime.now());
        System.out.println(format2); // 2020-12-22 07:43:31  少了十二小时

        //解析
        TemporalAccessor parse1 = formatter3.parse("2020-12-22 07:43:31");
        System.out.println(parse1);

泛型

把元素的类型设计为一个参数,这个参数的类型叫做泛型。如:Collection<E>, List<E>, ArrayList<E> 这个<E>就叫做类型参数,即泛型

集合中使用泛型

集合在JDK5.0 都修改为带泛型的结构

  • 在实例化集合类时,可以指定具体泛型类型
  • 指定完后,在类或接口中凡是定义类和接口时内部(方法、构造器、属性)用到类的泛型的位置,都指定为实例化时的泛型类型
  • 泛型的类型必须是类,不能是基本数据类型
  • 如果实例化时没有指定。默认类型为java.lang.Object类型

如:

ArrayList<Integer> list = new ArrayList<Integer>();

JDK7 后就可以省略后面:ArrayList<Integer> list = new ArrayList<>();

遍历时可以用 增强型for循环

迭代器:

ArrayList<Integer> list = new ArrayList<Integer>();
...
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()){
    int score = iterator.next();
    System.out.println(score);
}

又如:

Map<String, Integer> map = new HashMap<String, Integer>();
map.put("Tom", 87);
//泛型的嵌套
Set<Map.Entry<String, Integer>> entry = map.entrySet();
Iterator<Map.Entry<String, Integer>> iterator = entry.iterator();
//容器的遍历
while(iterator.hasNext()){
   Map.Entry<String, Integer> e = iterator.next();
   String key = e.getKey();
   int value = e.getValue();
}

自定义泛型类、接口

  1. 泛型类可能有多个参数,应该把多个参数放进尖括号内。如:<E1, E2, E3>
  2. 泛型类的构造器:public Class(){} 而 public Class()<E>{} 是错误的
  3. 实例化后,操作原来反省位置的结构必须和指定的泛型类型相同
  4. 泛型不同的引用不能相互赋值
    • 编译时 ArrayList<String> 和 ArrayList<Integer> 是两种类型,虽然在运行时只有一个ArrayList 被加载到JVM中
  5. 如果定义了泛型类,实例化没有指明,则认为泛型类是Object类型,但是不是和Object 等价
  6. 如果定义了泛型,那么实例化时要指明类的泛型 经验之谈
  7. 定义时 可以简化操作:Order、 oo = new Order<>();
  8. 不能使用基本数据类型
  9. 在类/接口 声明的泛型,在本类或本接口代表某种类型,可以作为非静态属性的类型,非静态方法的参数类型、非静态方法返回值类型。在静态方法中,不能使用泛型(泛型是在实例化时指定的,静态加载早于实例化)
  10. 异常类不能时泛型
  11. 不能使用 new E[] = new E[10]。但是可以E[] element = (E[])new Object[10];
public class Main {
    public static void main(String[] args) {
        Order o = new Order();
        o.seto("123456");
        String s1 = (String)o.geto(); //下转型
        System.out.println(s1);
        
        Order<String> oo = new Order<>();
        oo.seto("123");
        System.out.println(oo.geto());
    }
}

class Order<T> {
    String s1;
    T o;

    public Order() {
    }

    public Order(String s1, T o) {
        this.o = o;
    }

    public T geto() {
        return o;
    }

    public void seto(T o) {
        this.o = o;
    }
}

泛型继承问题

情况一:

public class SubOrder extends Order<Integer>{}

那么在声明时就不用加<> 因为指明了,就不是泛型了

情况二:

public class SubOrder<T> extends Order<T>{}

仍然是泛型类

子类不保留父类泛型:按需实现

  • 没有类型 擦除
  • 具体类型

子类保留父类泛型:泛型子类

  • 全部保留
  • 部分保留
class Father<T1, T2> {

}
class Son1 extends Father {
    //没有类型 擦除
}
class Son2 extends Father<String, Integer> {
    //具体类型
}
class Son3<T1, T2> extends Father<T1, T2> {
    //全部保留
} 
class Son4<T2> extends Father<Integer, T2> {
    //部分保留
}
class Son<T1, T2, A, B> extends Father<T1, T2>{
    
}
class Son2<T2, A, B> extends Father<Integer, T2>{
    
}

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

Object obj = null;
String s1 = null;
obj = str;

Object o[] = new Object();
String s[] = new String();
o = s;

List<Object> l1 = null;
List<String> l2 = null;
l1 = l2; //报错,因为不具有子父类关系

所以这种情况不具备子父类关系

如果是

List<String> list1 = null;
ArrayList<String> list2 = null;
list1 = list2; // 可以

可以

泛型方法

在方法中出现了泛型的结构,和类、接口是否使用泛型无关

//数组转化为List
public <E> List<E> copyL(E[] arr){
    ArrayList<E> list = new ArrayList<>();
    for(E e : arr){
        list.add(e)
    }
    return list;
}

不加最前面的<E>编译器会把E当作一个类,所以会报错

调用时指定泛型参数类型

可以声明为静态的,因为参数是在调用方法时确定的,不是在实例化类的时候确定的

泛型方法的应用

共用操作卸载一个类里面,然后不同的类去继承,继承时指明泛型类型

通配符的使用

通配符:?

List<Object> list1 = null;
List<String> list2 = null;
List<?> list = null;
list = list1;
list = list2;

那么1和2的父类是list

public void print(List<?> list){
    Iterator<?> iterator = list.iterator();
    while(iterator.hasNext()){
        Object obj = iterator.next();
        System.out.println(obj);
    }
}

实现了通用的调用。

使用通配符数据后的读取和写入要求
List<?> list = null;
List<String> list3 = new ArrayList<>();
list3.add("123");
list = list3;

//对于List<?> 不能向其内部添加数据
//除了添加null外
list.add(null);//毕竟所有类类型都可以为null

//获取(允许读取数据,类型为Object)
Object o = list.get(0); // 无论什么类型,总可以是Object
有限制条件的通配符的使用
public static void main(String[] args) {
        List<? extends Person> list1 = null; //看作小于等于
        List<? super Person> list2 = null; //看作大于等于

        List<Student> list3 = new ArrayList<Student>();
        List<Person> list4 = new ArrayList<Person>();
        List<Object> list5 = new ArrayList<Object>();

        list1 = list3;
        list1 = list4;
    //    list1 = list5; //报错
    //    list2 = list3; //报错
        list2 = list5;
    //读取
        list1 = list3;
        Person p = list1.get(0);//因为类型是小于等于Person的 所以可以用Person接收, 不能是Student
        list2 = list4;
        Object o = list2.get(0); // 只能用Object
    
    //写数据
        list1.add(new Student()); //编译不通过
        list1.add(new Person()); //编译不通过
        
        list2.add(new Person()); //因为类至少都是Person 以及其父类,所以Person 及一下的类
        list2.add(new Student());
    }

G<? extends A> 可以看作G<A> 和 G<B> 的父类,其中B是A的子类

G<? super A> 可以看作G<A> 和 G<B> 的父类,其中B是A的父类


Java 比较器

自然排序:java.lang.Comparable
  1. String、包装类等实现了Comparable 接口, 重写了 compareTo() 方法
  2. 重写规则:
    • 当前对象大于形参对象,返回正整数,否则返回负整数。相等返回零
    • 让自定义类实现Comparable接口,重写compareTo() 方法
class Goods implements Comparable {
    private String name;
    private double price;
...(省略构造器 set get 等方法)
    
    //指明商品比较大小的方式: 按照价格从低到高排序 从高到低可以加负号
    //比如如果价格相同再按照产品名称从高到低排序
    @Override
    public int compareTo(Object o) {
        if (o instanceof Goods) {
            Goods goods = (Goods) o;

            if (this.price > goods.price) {
                return 1;
            } else if (this.price == goods.price) {
                // return 0;
                return -this.name.compareTo(goods.name);
            } else {
                return -1;
            }
            //方式二:
            // return Double.compare(this.price, goods.price);
        }
        throw new RuntimeException("传人的数据类型不一致");
    }
}
定制排序:java.util.Comparator
  • 当元素类型没有实现java.lang.Comparable 接口而又不方便修改代码(如JDK 中的代码),或者实现了java.lang.Comparable 接口的排序规则不适合当前的操作,那可以考虑使用Comparator 的对象来排序,强行对多个对象进行整体排序的比较
  • 重写 compare(Object o1, Object o2) 方法,比较o1 o2 的大小:如果当前对象大于形参对象,返回正整数,否则返回负整数。相等返回零
  • 可以将 Comparator 传给sort方法(Collections.sort 或 Arrays.sort), 从而允许在顺序上实现精确控制
  • 还可以控制某些数据结构(如有序set或有序映射)的顺序,或者为那些没有自然顺序的对象collection 提供排序
String []s = {"123", "456", "4151", "sad", "sadas"};
        Arrays.sort(s, new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                if(o1 instanceof String && o2 instanceof String) {
                    String s1 = (String) o1;
                    String s2 = (String) o2;
                    return -s1.compareTo(s2);
                }
                throw new RuntimeException("输入类型不一致");
               // return 0;
            }
        });

//泛型写法
Arrays.sort(s2, new Comparator<Goods>() {
            @Override
            public int compare(Goods o1, Goods o2) {
                if(o1 instanceof Goods && o2 instanceof Goods) {
                    Goods g1 = (Goods) o1;
                    Goods g2 = (Goods) o2;
                    if(g1.getName().equals(g2.getName())) {
                        //价格从高到底
                        return -Double.compare(g1.getPrice(), g2.getPrice());
                    }else {
                        return g1.getName.compareTo(g2.getName());
                    }
                }
                throw new RuntimeException("输入类型不一致");
            }
        });

就像 C++的bool cmp() ;

对比:

  1. comparable 接口实现了可以让对象在任何位置可以比较大小
  2. comparator 是临时性的比较

System 类

内部成员变量和方法都是static 方便调用

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

成员方法:

long currentTimeMillis(); 返回1970 0000到现在差的毫秒数

exit(int status); 直接退出程序,status 为0为正常退出

void gc() 请求系统进行垃圾回收

String getProperty(String key); 获取系统中属性名为key的属性对应的值。

  • java.version 运行时环境版本
  • java.home java 安装目录
  • os.name 操作系统名称
  • os.version 操作系统版本
  • user.name 用户的账户名称
  • user.home 用户的主目录
  • user.dir 用户当前工作目录

Math 类

静态方法,参数和返回值一般为double 类型

abs() acos() asin() atan() cos() sin() tan()

sqrt() pow(double a, double b)

log() exp() max() min() random() 返回0.0到1.0的随机数

long round(double a) double 转换为long (四舍五入)

toDegrees(double angrad) 弧度 -> 角度

toRadians(double angdeg) 角度 -> 弧度

BigInteger 类

可以表示任意不可变的任意精度的整数

构造器(String val);

add substract multiply divide pow

remainder 返回(this & val)的BigInteger

BigDecimal 类

商业计算中,数字精度高的用到此类。

构造器:(double val) (String val)

add substract multiply divide


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值