java基础
第十章 多线程(基础)
10.1 相关的概念
1、程序(Program)
为了实现一个功能,完成一个任务而选择一种编程语言编写的一组指令的集合。
2、进程(Process)
程序的一次运行。操作系统会给这个进程分配资源(内存)。
进程是操作系统分配资源的最小单位。
进程与进程之间的内存是独立,无法直接共享。
最早的DOS操作系统是单任务的,同一时间只能运行一个进程。后来现在的操作系统都是支持多任务的,可以同时运行多个进程。进程之间来回切换。成本比较高。
3、线程(Thread)
线程是进程中的其中一条执行路径。一个进程中至少有一个线程,也可以有多个线程。有的时候也把线程称为轻量级的进程。
同一个进程的多个线程之间有些内存是可以共享的(方法区、堆),也有些内存是独立的(栈(包括虚拟机栈和本地方法栈)、程序计数器)。
线程之间的切换相对进程来说成本比较低。
4、并行: 多个处理器同时可以执行多条执行路径。
5、并发:多个任务同时执行,但是可能存在先后关系。
例子:
顺序执行:你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
并发:你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。
并行:你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行
理解:
解释一:并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
解释二:并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
解释三:在一台处理器上“同时”处理多个任务,在多台处理器上同时处理多个任务。如hadoop分布式集群
普通解释:
并发:交替做不同事情的能力
并行:同时做不同事情的能力
专业术语:
并发:不同的代码块交替执行
并行:不同的代码块同时执行
并发和并行的意义:
并发和并行都可以处理“多任务”,二者的主要区别在于是否是“同时进行”多个的任务。
但是 涉及到任务分解(有先后依赖的任务就不能做到并行)、任务运行(可能要考虑互斥、锁、共享等)、结果合并
10.2 两种实现多线程的方式
1、继承Thread类
步骤:
(1)编写线程类,去继承Thread类
(2)重写public void run(){}
(3)创建线程对象
(4)调用start()
class MyThread extends Thread {
public void run(){
//...
}
}
class Test{
public static void main(String[] args){
MyThread my = new MyThread();
my.start();//有名字的线程对象启动
new MyThread().start();//匿名线程对象启动
//匿名内部类的匿名对象启动
new Thread(){
public void run(){
//...
}
}.start();
//匿名内部类,但是通过父类的变量多态引用,启动线程
Thread t = new Thread(){
public void run(){
//...
}
};
t.start();
}
}
2、实现Runnable接口
步骤:
(1)编写线程类,实现Runnable接口
(2)重写public void run(){}
(3)创建线程对象
(4)借助Thread类的对象启动线程
class MyRunnable implements Runnable{
public void run(){
//...
}
}
class Test {
public static void main(String[] args){
MyRunnable my = new MyRunnable();
Thread t1 = new Thread(my);
Thread t2 = new Thread(my);
t1.start();
t2.start();
//两个匿名对象
new Thread(new MyRunnable()).start();
//匿名内部类的匿名对象作为实参直接传给Thread的构造器
new Thread(new Runnable(){
public void run(){
//...
}
}).start();
}
}
10.3 线程的生命周期
10.4 Thread的相关API
1、构造器
- Thread()
- Thread(String name)
- Thread(Runnable target)
- Thread(Runnable target, String name)
2、其他方法
(1)public void run()
(2)public void start()
(3)获取当前线程对象:Thread.currentThread()
(4)获取当前线程的名称:getName()
(5)设置或获取线程的优先级:set/getPriority()
优先级的范围:[1,10],Thread类中有三个常量:MAX_PRIORITY(10),MIN_PRIORITY(1),NORM_PRIORITY(5)
优先级只是影响概率。
(6)线程休眠:Thread.sleep(毫秒)
(7)打断线程:interrupt()
(8)暂停当前线程:Thread.yield()
(9)线程要加塞:join()
xx.join()这句代码写在哪个线程体中,哪个线程被加塞,和其他线程无关。
(10)判断线程是否已启动但未终止:isAlive()
1、方法
import org.junit.Test;
/*
* java.lang.Thread类的API:
* (1)public void run():子类必须重写,它的方法体也称为线程体,即线程的任务代码
* (2)public void start():线程启动必须用它
* (3)public static void sleep(毫秒):休眠
* (4)public String getName():线程的名称
* 主线程的名称:main
* 其他线程:默认是Thread-编号
* (5)public static Thread currentThread()
* (6)线程优先级
* getPriority()
* setPriority()
* 优先级的范围:MIN_PRIORITY - MAX_PRIORITY ,[1,10]
* 普通优先级:NORM_PRIORITY
* 一共10个等级。
* 优先级高:被CPU调度的概率增加,不表示低的没有机会。
* 所以:不能依赖于优先级来解决先后的任务问题。
*
* (7)public void interrupt()
* (8)public void join():加塞
* (9)public static void yield() :暂停当前线程,让出本次的CPU资源,加入下一次CPU的抢夺中
*/
public class TestMethod {
@Test
public void testJoin() {
MyRunnable my = new MyRunnable();
Thread t = new Thread(my);
t.start();
MyRunnable my2 = new MyRunnable();
Thread t2 = new Thread(my2);
t2.start();
for (int i = 1; i <= 10; i++) {
System.out.println("main:" + i);
if(i==3){
try {
t.join();//当main线程打印到3后,被t线程加塞,main线程就不能继续,main被阻塞了,main要等到t线程结束才能继续了
} catch (InterruptedException e) {
e.printStackTrace();
}
// Thread.yield();//作用不大,让出CPU,但下次有可能还是它抢到,象征性让一下
}
}
}
@Test
public void testInterrupt(){
MyThread my1= new MyThread();
my1.start();
//主线程休眠3秒后,中断MyThread线程
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
my1.interrupt();
}
@Test
public void testPriority(){
Thread t = Thread.currentThread();
System.out.println(t.getPriority());
MyThread my1= new MyThread();
System.out.println(my1.getPriority());
System.out.println("最高优先级:" + Thread.MAX_PRIORITY);
System.out.println("最低优先级:" +Thread.MIN_PRIORITY);
System.out.println("普通优先级:" + Thread.NORM_PRIORITY);
}
@Test
public void testName2(){
Thread t = Thread.currentThread();
System.out.println(t.getName());
MyThread my1= new MyThread();
System.out.println(my1.getName());
MyThread my2 = new MyThread();
System.out.println(my2.getName());
MyThread my3 = new MyThread("线程3");
System.out.println(my3.getName());
}
@Test
public void testName(){
Thread t = Thread.currentThread();
System.out.println(t.getName());
}
@Test
public void testSleep2(){
//获取明天的当前时间
try {
Thread.sleep(24*60*60*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(System.currentTimeMillis());
}
@Test
public void testSleep(){
for (int i = 10; i>=1; i--) {
System.out.println(i);
try {
Thread.sleep(1000);//毫秒 1000毫秒= 1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class MyThread extends Thread{
public MyThread() {
super();
}
public MyThread(String name) {
super(name);
}
public void run(){
System.out.println("自定义线程");
try {
Thread.sleep(10000);//休眠10秒
} catch (InterruptedException e) {
System.out.println("自定义线程被打断");
e.printStackTrace();
}
System.out.println("自定义线程休眠结束");
}
}
class MyRunnable implements Runnable{
public void run(){
for (int i = 10; i>=1; i--) {
System.out.println(Thread.currentThread().getName() + "run:" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
2、龟兔赛跑1
/*
2、案例:编写龟兔赛跑多线程程序,设赛跑长度为30米
兔子的速度是10米每秒,兔子每跑完10米休眠的时间10秒
乌龟的速度是1米每秒,乌龟每跑完10米的休眠时间是1秒
要求,
(1)每跑1米,显示一下结果:xxx跑了几米,
休息时,显示一下:xxx在休息...
(2)要求必须等乌龟和兔子都跑完了,然后显示结果谁赢了,用时xx时间
*/
public class TestExer07 {
public static void main(String[] args) {
Sportman t = new Sportman("兔子",30,100,10000);
Sportman w = new Sportman("乌龟",30,1000,1000);
t.start();
w.start();
try {
//等t和w线程结束后,才能运行下面的代码
t.join();//t阻塞了main
w.join();//w阻塞了main
//不加上这两句的话,两个动物还没跑完主线程就往下运行了
} catch (InterruptedException e) {
e.printStackTrace();
}
//在这里想要获取运动员跑全程的时间
long tTime = t.getTotalTime();
long wTime = w.getTotalTime();
if(tTime == wTime){
System.out.println(t.getName() + "," + w.getName() + "平局");
}else if(tTime < wTime){
System.out.println(t.getName() + "赢了");
}else{
System.out.println(w.getName() + "赢了");
}
System.out.println(t.getName() + "用时:" + tTime);
System.out.println(w.getName() + "用时:" + wTime);
}
}
class Sportman extends Thread{
private int distance;//距离
private long runMillsPerMeter;//每米的时间,毫秒
private long restMillsPerTenMeter;//每10米休息的时间,毫秒
private long totalTime;
public Sportman(String name, int distance ,long millsPerMeter, long restPerTenMeter) {
super(name);
this.distance = distance;
this.runMillsPerMeter = millsPerMeter;
this.restMillsPerTenMeter = restPerTenMeter;
}
public void run(){
long start = System.currentTimeMillis();
for (int i = 1; i <= distance; i++) {
try {
Thread.sleep(runMillsPerMeter);//模拟跑1米的时间
} catch (InterruptedException e) {
e.printStackTrace();
}
//用线程名称代替运动员的名称
System.out.println(getName() + "跑了" + i + "米");
if(i<distance && i%10==0){
System.out.println(getName() + "在休息....");
try {
Thread.sleep(restMillsPerTenMeter);//休息n秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
long end = System.currentTimeMillis();
totalTime = end - start;
}
public long getTotalTime() {
return totalTime;
}
}
10.5 关键字:volatile
volatile:易变,不稳定,不一定什么时候会变
修饰:成员变量
作用:当多个线程同时去访问的某个成员变量时,而且是频繁的访问,再多次访问时,发现它的值没有修改,Java执行引擎就会对这个成员变量的值进行缓存。一旦缓存之后,这个时候如果有一个线程把这个成员变量的值修改了,Java执行引擎还是从缓存中读取,导致这个值不是最新的。如果不希望Java执行引擎把这个成员变的值缓存起来,那么就可以在成员变量的前面加volatile,每次用到这个成员变量时,都是从主存中读取。
原子性问题,可见性问题,有序性问题,volatile深入刨析
10.6 关键字:synchronized(同步)
1、什么情况下会发生线程安全问题?
(1)多个线程
(2)共享数据
(3)多个线程的线程体中,多条语句再操作这个共享数据时
2、如何解决线程安全问题?同步锁
形式一:同步代码块
形式二:同步方法
3、同步代码块
synchronized(锁对象){
//一次任务代码,这其中的代码,在执行过程中,不希望其他线程插一脚
}
锁对象:
(1)任意类型的对象
(2)确保使用共享数据的这几个线程,使用同一个锁对象
如类中的变量,类.class
4、同步方法
synchronized 【修饰符】 返回值类型 方法名(【形参列表】)throws 异常列表{
//同一时间,只能有一个线程能进来运行
}
锁对象:
(1)非静态方法:this(谨慎)
(2)静态方法:当前类的Class对象
public class Test11 {
public static void main(String[] args) {
Ticket t1 = new Ticket("窗口一");
Ticket t2 = new Ticket("窗口二");
Ticket t3 = new Ticket("窗口三");
t1.start();
t2.start();
t3.start();
}
}
class Ticket extends Thread{
private static int total = 10;
public Ticket(String name) {
super(name);
}
public void run(){
while(total>0){//程序停止的条件
saleOneTicket();
}
}
public synchronized static void saleOneTicket(){
if(total > 0){//线程安全问题的条件
System.out.println(Thread.currentThread().getName() + "卖出一张票");
total--;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("剩余:" + total);
}
}
//同步方法,锁的是方法的一次调用过程
//非静态方法的锁对象是this,这里使用this,不是合格的锁对象
/*public synchronized void saleOneTicket(){
if(total > 0){//线程安全问题的条件
System.out.println(getName() + "卖出一张票");
total--;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("剩余:" + total);
}
}*/
}
5、死锁
/*
* 死锁:
* 两个线程,互相持有,占有对方想要的锁,不放手。
*/
public class TestDeadLock {
public static void main(String[] args) {
Object g = new Object();
Object m = new Object();
Boy b = new Boy(g,m);
Bang bang = new Bang(g,m);
b.start();
bang.start();
}
}
//男朋友
class Boy extends Thread{
private Object girl;
private Object money;
public Boy(Object girl, Object money) {
super();
this.girl = girl;
this.money = money;
}
public void run(){
synchronized (money) {
System.out.println("你放了我女朋友,我给你500万");
synchronized (girl) {
System.out.println("给了对方500万");
}
}
}
}
//绑匪
class Bang extends Thread{
private Object girl;
private Object money;
public Bang(Object girl, Object money) {
super();
this.girl = girl;
this.money = money;
}
public void run(){
synchronized (girl) {
System.out.println("你给我500万,我放了你女朋友");
synchronized (money) {
System.out.println("放人");
}
}
}
}
10.7 线程通信
1、为了解决“生产者与消费者问题”。
当一些线程负责往“数据缓冲区”放数据,另一个线程负责从“数据缓冲区”取数据。
问题1:生产者线程与消费者线程使用同一个数据缓冲区,就是共享数据,那么要考虑同步
问题2:当数据缓冲区满的时候,生产者线程需要wait(), 当消费者消费了数据后,需要notify或notifyAll
当数据缓冲区空的时候,消费者线程需要wait(), 当生产者生产了数据后,需要notify或notifyAll
2、java.lang.Object类中声明了:
(1)wait():必须由“同步锁”对象调用
(2)notfiy()和notifyAll():必须由“同步锁”对象调用
3、面试题:sleep()和wait的区别
(1)sleep()不释放锁,wait()释放锁
(2)sleep()在Thread类中声明的,wait()在Object类中声明
(3)sleep()是静态方法,是Thread.sleep()
wait()是非静态方法,必须由“同步锁”对象调用
(4)sleep()方法导致当前线程进入阻塞状态后,当时间到或interrupt()醒来
wait()方法导致当前线程进入阻塞状态后,由notify或notifyAll()
4、哪些操作会释放锁?
(1)同步代码块或同步方法正常执行完一次自动释放锁
(2)同步代码块或同步方法遇到return等提前结束
(3)wait()
(4)遇到异常,但是没有处理,这个线程死了,会释放锁
5、不释放锁
(1)sleep()
(2)yield()
(3)suspend()
6、消费者、生产者
(1)一个生产者、一个消费者
package com.atguigu.test14;
/*
* 线程通信是用来解决生产者与消费者问题。
*
* 生产者与消费者问题:
* 有两个或者多个线程。
* 其中一个/一部分线程,生产“数据”,称为生产者线程;
* 另一个/一部分线程,消耗“数据”,称为消费者线程。
* 这些数据放在一个“共享”区域。
* 那么就会出现:
* 当“共享”区域中的数据空了,消费者线程必须"停/等待",等待到产者线程生产了新数据后,继续进行。
* 当“共享”区域中的数据满了,生产者线程必须"停下/等待",等到消费者线程消耗了数据后,继续进行。
*
* 生产者与消费者问题:
* (1)共享数据: 就会有线程安全问题,就需要同步
* (2)共享区域大小固定,有限的:就需要用到“协作”,线程通信。
*
* Object类中有:
* (1)wait():必须由锁对象(线程的监视器对象)来调用。
* (2)notify():必须由锁对象(线程的监视器对象)来调用。
* notify()作用就是唤醒一个正在等待的线程。唤醒的是同一个锁对象监视的等待线程。
*/
public class Test14 {
public static void main(String[] args) {
Workbench tai = new Workbench();
Cook c = new Cook("崔志恒", tai);
Waiter w = new Waiter("翠花", tai);
c.start();
w.start();
}
}
class Workbench{
//假设工作台上最多能够放10盘
private static final int MAX = 10;
private int count;
//同步方法,非静态方法来说,锁对象就是this
public synchronized void put(){//往工作台上放一盘菜
if(count >= MAX){
try {
//生产者停下来,等待
wait();//默认是this.wait()
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count++;
System.out.println(Thread.currentThread().getName() + "放了一盘菜,剩余:" + count);
this.notify();
}
public synchronized void take(){//从工作台上取走一盘菜
if(count<=0){
try {
//工作台没有菜,消费者应该停下来
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count--;
System.out.println(Thread.currentThread().getName() + "取走一盘菜,剩余:" + count);
this.notify();
}
}
class Cook extends Thread{
private Workbench tai;
public Cook(String name, Workbench tai) {
super(name);
this.tai = tai;
}
public void run(){
while(true){
tai.put();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Waiter extends Thread{
private Workbench tai;
public Waiter(String name, Workbench tai) {
super(name);
this.tai = tai;
}
public void run(){
while(true){
tai.take();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
(2)多个生产者与消费者
如果用if,有可能此时消费者秋香和翠花都在wait()等待,且count=0。
生产者放了一个后,count=1,并唤醒了翠花。
翠花取了一个后count=0,接着翠花唤醒了秋香,秋香直接向下执行,就会导致count=-1
所以应该改为用while
如果为notify()
①当MAX=1,count=1,两个生产者全部wait
②消费者秋香取一个,count=0,并唤醒小崔,同时秋香再次抢到锁并wait
③消费者如花拿到锁,直接wait()
生产者小崔放一个,count=1,并唤醒小甄,同时再次抢到锁,wait
④小甄拿到锁直接wait()
四个全部wait卡住
所以需要notifyAll()
package com.atguigu.test16;
/*
* 线程通信是用来解决生产者与消费者问题。
*
* 生产者与消费者问题:
* 有两个或者多个线程。
* 其中一个/一部分线程,生产“数据”,称为生产者线程;
* 另一个/一部分线程,消耗“数据”,称为消费者线程。
* 这些数据放在一个“共享”区域。
* 那么就会出现:
* 当“共享”区域中的数据空了,消费者线程必须"停/等待",等待到产者线程生产了新数据后,继续进行。
* 当“共享”区域中的数据满了,生产者线程必须"停下/等待",等到消费者线程消耗了数据后,继续进行。
*
* 生产者与消费者问题:
* (1)共享数据: 就会有线程安全问题,就需要同步
* (2)共享区域大小固定,有限的:就需要用到“协作”,线程通信。
*
* Object类中有:
* (1)wait():必须由锁对象(线程的监视器对象)来调用。
* (2)notify():必须由锁对象(线程的监视器对象)来调用。
* notify()作用就是唤醒一个正在等待的线程。唤醒的是同一个锁对象监视的等待线程。
* (3)notifyAll():唤醒所有和我是同一个监视器对象的正在等待的线程
*/
public class Test16 {
public static void main(String[] args) {
Workbench tai = new Workbench();
Cook c1 = new Cook("崔志恒", tai);
Cook c2 = new Cook("甄玉禄", tai);
Waiter w1 = new Waiter("翠花", tai);
Waiter w2 = new Waiter("如花", tai);
// Waiter w3 = new Waiter("秋香", tai);
c1.start();
c2.start();
w1.start();
w2.start();
// w3.start();
}
}
class Workbench{
//假设工作台上最多能够放10盘
private static final int MAX = 1;
private int count;
//同步方法,非静态方法来说,锁对象就是this
public synchronized void put(){//往工作台上放一盘菜
while(count >= MAX){
try {
//生产者停下来,等待
wait();//默认是this.wait()
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count++;
System.out.println(Thread.currentThread().getName() + "放了一盘菜,剩余:" + count);
this.notify();
}
public synchronized void take(){//从工作台上取走一盘菜
while(count<=0){
try {
//工作台没有菜,消费者应该停下来
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count--;
System.out.println(Thread.currentThread().getName() + "取走一盘菜,剩余:" + count);
// this.notify();
this.notifyAll();
}
}
class Cook extends Thread{
private Workbench tai;
public Cook(String name, Workbench tai) {
super(name);
this.tai = tai;
}
public void run(){
while(true){
tai.put();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Waiter extends Thread{
private Workbench tai;
public Waiter(String name, Workbench tai) {
super(name);
this.tai = tai;
}
public void run(){
while(true){
tai.take();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
第十一章 常用类
11.1 包装类
11.1.1 包装类
当要使用只针对对象设计的API或新特性(例如泛型),那么基本数据类型的数据就需要用包装类来包装。
序号 | 基本数据类型 | 包装类 |
---|---|---|
1 | byte | Byte |
2 | short | Short |
3 | int | Integer |
4 | long | Long |
5 | float | Float |
6 | double | Double |
7 | char | Character |
8 | boolean | Boolean |
9 | void | Void |
11.1.2 装箱与拆箱
JDK1.5之后,可以自动装箱与拆箱。
注意:只能与自己对应的类型之间才能实现自动装箱与拆箱。
Integer i = 1;
Double d = 1;//错误的,1是int类型
装箱:把基本数据类型转为包装类对象。
转为包装类的对象,是为了使用专门为对象设计的API和特性
拆箱:把包装类对象拆为基本数据类型。
转为基本数据类型,一般是因为需要运算,Java中的大多数运算符是为基本数据类型设计的。比较、算术等
总结:对象(引用数据类型)能用的运算符有哪些?
(1)instanceof
(2)=:赋值运算符
(3)==和!=:用于比较地址,但是要求左右两边对象的类型一致或者是有父子类继承关系。
(4)对于字符串这一种特殊的对象,支持“+”,表示拼接。
11.1.3 包装类的一些API
1、基本数据类型和字符串之间的转换
(1)把基本数据类型转为字符串
int a = 10;
//String str = a;//错误的
//方式一:
String str = a + "";
//方式二:
String str = String.valueOf(a);
(2)把字符串转为基本数据类型
int a = Integer.parseInt("整数的字符串");
double a = Double.parseDouble("小数的字符串");
boolean b = Boolean.parseBoolean("true或false");
2、数据类型的最大最小值
Integer.MAX_VALUE和Integer.MIN_VALUE
Long.MAX_VALUE和Long.MIN_VALUE
Double.MAX_VALUE和Double.MIN_VALUE
3、转大小写
Character.toUpperCase('x');
Character.toLowerCase('X');
4、转进制
Integer.toBinaryString(int i)
Integer.toHexString(int i)
Integer.toOctalString(int i)
11.1.4 包装类对象的缓存问题
包装类 | 缓存对象 |
---|---|
Byte | -128~127 |
Short | -128~127 |
Integer | -128~127 |
Long | -128~127 |
Float | 没有 |
Double | 没有 |
Character | 0~127 |
Boolean | true和false |
Integer i = 1;
Integer j = 1;
System.out.println(i == j);//true
Integer i = 128;
Integer j = 128;
System.out.println(i == j);//false
Integer i = new Integer(1);//新new的在堆中
Integer j = 1;//这个用的是缓冲的常量对象,在方法区
System.out.println(i == j);//false
Integer i = new Integer(1);//新new的在堆中
Integer j = new Integer(1);//另一个新new的在堆中
System.out.println(i == j);//false
Integer i = new Integer(1);
int j = 1;
System.out.println(i == j);//true,凡是和基本数据类型比较,都会先拆箱,按照基本数据类型的规则比较
11.2 字符串
11.2.1 字符串的特点
1、字符串String类型本身是final声明的,意味着我们不能继承String。
2、字符串的对象也是不可变对象,意味着一旦进行修改,就会产生新对象
我们修改了字符串后,如果想要获得新的内容,必须重新接受。
如果程序中涉及到大量的字符串的修改操作,那么此时的时空消耗比较高。可能需要考虑使用StringBuilder或StringBuffer。
3、String对象内部是用字符数组进行保存的
JDK1.9之前有一个char[] value数组,JDK1.9之后byte[]数组
4、String类中这个char[] values数组也是final修饰的,意味着这个数组不可变,然后它是private修饰,外部不能直接操作它,String类型提供的所有的方法都是用新对象来表示修改后内容的,所以保证了String对象的不可变。
5、就因为字符串对象设计为不可变,那么所以字符串有常量池来保存很多常量对象
常量池位置:
(1)JDK1.6及其之前:永久代(方法区的一种实现)
(2)JDK1.7:字符串常量池单独在堆,其他在永久代
(3)JDK1.8:字符串常量池单独在堆,其他在元空间
PermGen(永久代)----->替换为Metaspace(元空间)(本地内存中)
方法区和“PermGen space”又有着本质的区别。前者是 JVM 的规范,而后者则是 JVM 规范的一种实现,并且只有 HotSpot 才有 “PermGen space”,而对于其他类型的虚拟机,如 JRockit(Oracle)、J9(IBM) 并没有“PermGen space”。由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过设置一些参数来指定元空间的大小
11.2.2 字符串对象的比较
1、==:比较是对象的地址
只有两个字符串变量都是指向字符串的常量对象时,才会返回true
String str1 = "hello";
String str2 = "hello";
str1 == str2//true
2、equals:比较是对象的内容,因为String类型重写equals,区分大小写
只要两个字符串的字符内容相同,就会返回true
String str1 = new String("hello");
String str2 = new String("hello");
str1.equals(strs) //true
3、equalsIgnoreCase:比较的是对象的内容,不区分大小写
String str1 = new String("hello");
String str2 = new String("HELLO");
str1.equalsIgnoreCase(strs) //true
4、compareTo:String类型重写了Comparable接口的抽象方法,自然排序,按照字符的Unicode编码值进行比较大小的,严格区分大小写
String str1 = "hello";
String str2 = "world";
str1.compareTo(str2) //小于0的值
5、compareToIgnoreCase:不区分大小写,其他按照字符的Unicode编码值进行比较大小
String str1 = new String("hello");
String str2 = new String("HELLO");
str1.compareToIgnoreCase(str2) //等于0
11.2.3 空字符的比较
1、哪些是空字符串
String str1 = "";
String str2 = new String();
String str3 = new String("");
空字符串:长度为0
2、如何判断某个字符串是否是空字符串
if("".equals(str))
if(str!=null && str.isEmpty())
if(str!=null && str.equals(""))
if(str!=null && str.length()==0)
11.2.4 字符串的对象的个数
1、字符串常量对象
String str1 = "hello";//1个,在常量池中
2、字符串的普通对象
String str2 = new String();
String str22 = new String("");
//两个对象,一个是常量池中的空字符串对象,一个是堆中的空字符串对象
3、字符串的普通对象和常量对象一起
String str3 = new String("hello");
//str3首先指向堆中的一个字符串对象,然后堆中字符串的value数组指向常量池中常量对象的value数组
11.2.5 字符串拼接结果
原则:
(1)常量+常量:结果是常量池
(2)常量与变量 或 变量与变量:结果是堆
(3)拼接后调用intern方法:结果在常量池
@Test
public void test06(){
String s1 = "hello";
String s2 = "world";
String s3 = "helloworld";
String s4 = (s1 + "world").intern();//把拼接的结果放到常量池中
String s5 = (s1 + s2).intern();
System.out.println(s3 == s4);//true
System.out.println(s3 == s5);//true
}
@Test
public void test05(){
final String s1 = "hello";
final String s2 = "world";
String s3 = "helloworld";
String s4 = s1 + "world";//s4字符串内容也helloworld,s1是常量,"world"常量,常量+ 常量 结果在常量池中
String s5 = s1 + s2;//s5字符串内容也helloworld,s1和s2都是常量,常量+ 常量 结果在常量池中
String s6 = "hello" + "world";//常量+ 常量 结果在常量池中,因为编译期间就可以确定结果
System.out.println(s3 == s4);//true
System.out.println(s3 == s5);//true
System.out.println(s3 == s6);//true
}
@Test
public void test04(){
String s1 = "hello";
String s2 = "world";
String s3 = "helloworld";
String s4 = s1 + "world";//s4字符串内容也helloworld,s1是变量,"world"常量,变量 + 常量的结果在堆中
String s5 = s1 + s2;//s5字符串内容也helloworld,s1和s2都是变量,变量 + 变量的结果在堆中
String s6 = "hello" + "world";//常量+ 常量 结果在常量池中,因为编译期间就可以确定结果
System.out.println(s3 == s4);//false
System.out.println(s3 == s5);//false
System.out.println(s3 == s6);//true
}
11.2.6 字符串的API
(1)boolean isEmpty()
(2)int length()
(3)String concat(xx):拼接,等价于+
(4)boolean contanis(xx)
(5)int indexOf():从前往后找,要是没有返回-1
(6)int lastIndexOf():从后往前找,要是没有返回-1
(7)char charAt(index)
(8)new String(char[] ) 或new String(char[] ,int, int)
(9)char[] toCharArray()
(10)byte[] getBytes():编码,把字符串变为字节数组,按照平台默认的字符编码进行编码
byte[] getBytes(字符编码方式):按照指定的编码方式进行编码
(11)new String(byte[] ) 或 new String(byte[], int, int):解码,按照平台默认的字符编码进行解码
new String(byte[],字符编码方式 ) 或 new String(byte[], int, int,字符编码方式):解码,按照指定的编码方式进行解码
(12)String substring(int begin):从[begin]开始到最后
String substring(int begin,int end):从[begin, end)
(13)boolean matchs(正则表达式)
(14)String replace(xx,xx):不支持正则
String replaceFirst(正则,value):替换第一个匹配部分
String repalceAll(正则, value):替换所有匹配部分
(15)String[] split(正则):按照某种规则进行拆分
(16)boolean startsWith(xx):是否以xx开头
boolean endsWith(xx):是否以xx结尾
(17)String trim():去掉前后空白符,字符串中间的空白符不会去掉
(18)String toUpperCase():转大写
(19)String toLowerCase():转小写
面试题:字符串的length和数组的length有什么不同?
字符串的length(),数组的length属性
11.3 可变字符序列
1、可变字符序列:StringBuilder和StringBuffer
StringBuffer:老的,线程安全的(因为它的方法有synchronized修饰)
StringBuilder:线程不安全的
2、面试题:String和StringBuilder、StringBuffer的区别?
String:不可变对象,不可变字符序列
StringBuilder、StringBuffer: 可变字符序列
3、常用的API,StringBuilder、StringBuffer的API是完全一致的
(1)append(xx):拼接,追加
(2)insert(int index, xx):插入
(3)delete(int start, int end)
deleteCharAt(int index)
(4)set(int index, xx)
(5)reverse():反转
… 替换、截取、查找…
11.4 和数学相关的
1、java.lang.Math类
(1)sqrt():求平方根
(2)pow(x,y):求x的y次方
(3)random():返回[0,1)范围的小数
(4)max(x,y):找x,y最大值
min(x,y):找最小值
(5)round(x):四舍五入
ceil(x):进一
floor(x):退一
…
2、java.math包
BigInteger:大整数
BigDecimal:大小数
运算通过方法完成:add(),subtract(),multiply(),divide()…
11.5 日期时间API
11.5.1 JDK1.8之前
1、java.util.Date
new Date():当前系统时间
long getTime():返回该日期时间对象距离1970-1-1 0.0.0 0毫秒之间的毫秒值
new Date(long 毫秒):把该毫秒值换算成日期时间对象
2、java.util.Calendar:
(1)getInstance():得到Calendar的镀锡
(2)get(常量)
3、java.text.SimpleDateFormat:日期时间的格式化
y:表示年
M:月
d:天
H: 小时,24小时制
h:小时,12小时制
m:分
s:秒
S:毫秒
E:星期
D:年当中的天数
@Test
public void test10() throws ParseException{
String str = "2019年06月06日 16时03分14秒 545毫秒 星期四 +0800";
SimpleDateFormat sf = new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒 SSS毫秒 E Z");
Date d = sf.parse(str);
System.out.println(d);
}
@Test
public void test9(){
Date d = new Date();
SimpleDateFormat sf = new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒 SSS毫秒 E Z");
//把Date日期转成字符串,按照指定的格式转
String str = sf.format(d);
System.out.println(str);
}
@Test
public void test8(){
String[] all = TimeZone.getAvailableIDs();
for (int i = 0; i < all.length; i++) {
System.out.println(all[i]);
}
}
@Test
public void test7(){
TimeZone t = TimeZone.getTimeZone("America/Los_Angeles");
//getInstance(TimeZone zone)
Calendar c = Calendar.getInstance(t);
System.out.println(c);
}
@Test
public void test6(){
Calendar c = Calendar.getInstance();
System.out.println(c);
int year = c.get(Calendar.YEAR);
System.out.println(year);
int month = c.get(Calendar.MONTH)+1;
System.out.println(month);
//...
}
@Test
public void test5(){
long time = Long.MAX_VALUE;
Date d = new Date(time);
System.out.println(d);
}
@Test
public void test4(){
long time = 1559807047979L;
Date d = new Date(time);
System.out.println(d);
}
@Test
public void test3(){
Date d = new Date();
long time = d.getTime();
System.out.println(time);//1559807047979
}
@Test
public void test2(){
long time = System.currentTimeMillis();
System.out.println(time);//1559806982971
//当前系统时间距离1970-1-1 0:0:0 0毫秒的时间差,毫秒为单位
}
@Test
public void test1(){
Date d = new Date();
System.out.println(d);
}
11.5.2 JDK1.8之后
java.time及其子包中。
1、LocalDate、LocalTime、LocalDateTime
(1)now():获取系统日期或时间
(2)of(xxx):或者指定的日期或时间
(3)运算:运算后得到新对象,需要重新接受
plusXxx():在当前日期或时间对象上加xx
minusXxx() :在当前日期或时间对象上减xx
方法 | 描述 |
---|---|
now() / now(ZoneId zone) | 静态方法,根据当前时间创建对象/指定时区的对象 |
of() | 静态方法,根据指定日期/时间创建对象 |
getDayOfMonth()/getDayOfYear() | 获得月份天数(1-31) /获得年份天数(1-366) |
getDayOfWeek() | 获得星期几(返回一个 DayOfWeek 枚举值) |
getMonth() | 获得月份, 返回一个 Month 枚举值 |
getMonthValue() / getYear() | 获得月份(1-12) /获得年份 |
getHours()/getMinute()/getSecond() | 获得当前对象对应的小时、分钟、秒 |
withDayOfMonth()/withDayOfYear()/withMonth()/withYear() | 将月份天数、年份天数、月份、年份修改为指定的值并返回新的对象 |
with(TemporalAdjuster t) | 将当前日期时间设置为校对器指定的日期时间 |
plusDays(), plusWeeks(), plusMonths(), plusYears(),plusHours() | 向当前对象添加几天、几周、几个月、几年、几小时 |
minusMonths() / minusWeeks()/minusDays()/minusYears()/minusHours() | 从当前对象减去几月、几周、几天、几年、几小时 |
plus(TemporalAmount t)/minus(TemporalAmount t) | 添加或减少一个 Duration 或 Period |
isBefore()/isAfter() | 比较两个 LocalDate |
isLeapYear() | 判断是否是闰年(在LocalDate类中声明) |
format(DateTimeFormatter t) | 格式化本地日期、时间,返回一个字符串 |
parse(Charsequence text) | 将指定格式的字符串解析为日期、时间 |
2、DateTimeFormatter:日期时间格式化
该类提供了三种格式化方法:
预定义的标准格式。如:ISO_DATE_TIME;ISO_DATE
本地化相关的格式。如:ofLocalizedDate(FormatStyle.MEDIUM)
自定义的格式。如:ofPattern(“yyyy-MM-dd hh:mm:ss”)
@Test
public void test10(){
LocalDateTime now = LocalDateTime.now();
// DateTimeFormatter df = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG);//2019年6月6日 下午04时40分03秒
DateTimeFormatter df = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT);//19-6-6 下午4:40
String str = df.format(now);
System.out.println(str);
}
@Test
public void test9(){
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter df = DateTimeFormatter.ISO_DATE_TIME;//2019-06-06T16:38:23.756
String str = df.format(now);
System.out.println(str);
}
@Test
public void test8(){
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH时mm分ss秒 SSS毫秒 E 是这一年的D天");
String str = df.format(now);
System.out.println(str);
}
@Test
public void test7(){
LocalDate now = LocalDate.now();
LocalDate before = now.minusDays(100);
System.out.println(before);//2019-02-26
}
@Test
public void test06(){
LocalDate lai = LocalDate.of(2019, 5, 13);
LocalDate go = lai.plusDays(160);
System.out.println(go);//2019-10-20
}
@Test
public void test05(){
LocalDate lai = LocalDate.of(2019, 5, 13);
System.out.println(lai.getDayOfYear());
}
@Test
public void test04(){
LocalDate lai = LocalDate.of(2019, 5, 13);
System.out.println(lai);
}
@Test
public void test03(){
LocalDateTime now = LocalDateTime.now();
System.out.println(now);
}
@Test
public void test02(){
LocalTime now = LocalTime.now();
System.out.println(now);
}
@Test
public void test01(){
LocalDate now = LocalDate.now();
System.out.println(now);
}
11.6 Leetcode常用补充
void System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length) src为原数组, dest为目标数组
long System.currentTimeMillis() 当前时间(毫秒)
Arrays.sort(T[] a, Comparator<? super T> c) c可以为lambda
Arrays.toString(Object[] a)
Arrays.copyOf(T[] original, int newLength)