Skip to content

一、复习

1.1 IO 流

4 个抽象基类:

  • InputStream:字节输入流

    • 方法:read()读 1 个字节、read(byte[] b)读多个字节....
    • FileInputStream:文件字节输入流,可以读取任意类型的文件。
    • ObjectInputStream:对象字节输入流,用于对象的反序列化。额外方法:readObject()readInt()readDouble()readUTF()....
    • ....
  • OutputStream:字节输出流

    • write(1个字节)write(byte[] b, int offset,int len)....
    • FileOutputStream:文件字节输出流,可以将任意类型的数据写到文件中,并且还可以指定追加模式
    • ObjectOutputStream:对象字节输出流,用于对象的序列化。额外方法:writeObject(对象)writeInt(int值)writeDouble(小数)writeUTF(字符串)....
    • PrintStream:打印流。额外的方法:print(各种类型的数据)println(各种类型的数据)
    • ....
  • Reader:字符输入流

    • 方法:read()读 1 个字符、read(char[] b)读多个字符....
    • FileReader:文件字符输入流,专门用于读取纯文本文件,并且可以在读取文件时指定文件的编码
    • ....
  • Writer:字符输出流

    • 方法:write(1个字符)write(char[] b, int offset,int len)....
    • FileWriter:文件字符输出流,专门用于将纯文本的内容写到纯文本文件中,并且可以在写文件时指定文件的编码,并且还可以指定追加模式
    • .....

所有 IO 流都有 close 方法,建议 IO 流使用完毕都正确关闭。

所有的输出流都有flush()方法,用于即时刷新数据。

对象序列化有什么注意事项:

  • 要序列化的对象的类型必须实现 Serializable 接口
  • 实现 Serializable 接口的同时要加一个序列化版本 ID:private static final long serialVersionUID = 值;
  • 默认 static、transient 修饰的成员变量不序列
  • 如果要定制序列化和反序列化过程,需要手动编写 2 个方法:writeObejct 和 readObject

1.2 异常

异常的根类型是:Throwable,它又分为 Error 和 Exception。

  • Error:StackOverflewError(栈内存溢出错误)等
  • Exception:
    • 受检异常或编译时异常:编译器可以提醒我们可能发生 xx 异常。代表:Throwable、Exception、IOException、FileNotFoundException、ClassNotFoundException 等
    • 非受检异常或运行时异常:编译器检查不出来。代表:RuntimeException、ArithmetciException(算术异常)、NullPointerExcetion(空指针异常)、ArrayIndexOutOfBoundsException(数组下标越界异常)、ClassCastException(类型转换异常)、InputMismatchException(输入不匹配异常)等

无论异常是哪种类型,它是编译时异常也好,运行时异常也好,只要真的发生了,如果不处理,都会导致程序崩溃。

只是编译时异常,编译器会提醒我们提前就写好 异常处理的代码,如果不编写如何处理的代码,编译就不通过,不让你运行。

说白了,运行时异常更考验程序员的经验,是否自己能提前预判可能有哪些风险,需要加条件判断避免 或 加 try-catch 处理。

异常处理的 5 个关键字:

java
//普通try-catch
try{
    可能发生异常的代码
}catch(异常的类型1 参数名e){
    异常处理的代码

    打印或记录异常信息的代码
}catch(异常的类型2 参数名e){
    异常处理的代码

    打印或记录异常信息的代码
}finally{
    必须执行的代码。一般是资源释放代码。
}
java
try(需要自动关闭的资源对象的声明){
    可能发生异常的业务代码
}catch(异常的类型2 参数名e){
    异常处理的代码

    打印或记录异常信息的代码
}finally{
    如果真有除了资源关闭之外的其他必须执行的代码,可以写这里。
}
throwthrows
作用主动抛出一个异常对象在方法头的签名中,声明当前方法可能发生 xx 异常,且当前方法没处理,交给调用者处理
语法形式throw 异常对象;【修饰符】 返回值类型 方法名(【形参列表】)throws 异常类型列表{}

二、日期时间

Java 中一共引入了三代的日期时间 API。

2.1 第 1 代

1、java.util.Date

Date 类中大部分方法都已过时。它的对象可以表示一个日期时间的瞬时。

java
package com.atguigu.date;

import org.junit.jupiter.api.Test;

import java.util.Date;

public class TestDate {
    @Test
    public void test(){
        Date now = new Date();
        System.out.println("now = " + now);
        //now = Tue Jul 15 09:00:56 CST 2025
    }

    @Test
    public void test2(){
        Date now = new Date();
        long time = now.getTime();
        //距离1970-1-1 8:0:0 的毫秒值
        System.out.println("time = " + time);
        //time = 1752541333556
    }

    @Test
    public void test3(){
        long time = System.currentTimeMillis();//更简洁
        //距离1970-1-1 8:0:0 的毫秒值
        System.out.println("time = " + time);
        //time = 1752541333556
    }

    @Test
    public void test4(){
        long start = System.currentTimeMillis();
        String str = "";
        for(int i=1; i<=100000; i++){
            str += i;
        }
        long end = System.currentTimeMillis();
        System.out.println("耗时:" + (end-start));//耗时:2853
    }

    @Test
    public void test5(){
        long time = 8645615122L;
        Date d = new Date(time);
        System.out.println("d = " + d);
        //d = Sat Apr 11 09:33:35 CST 1970
    }
}

2、java.text.SimpleDateFormat

SimpleDateFormat 用于对 Date 对象进行格式化,按照指定的模板将 Date 对象转为字符串,或将字符串按照指定的模板解析为 Date 对象。

image-20250715090754502

java
package com.atguigu.date;

import org.junit.jupiter.api.Test;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class TestSimpleDateFormat {
    @Test
    public void test1(){
        Date now = new Date();
        //我想要上面的now对象,显示为:2025年07月15日 09:06:56 星期几
        SimpleDateFormat sf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss E");
        String str = sf.format(now); //把Date对象转为字符串对象
        System.out.println(str);
        //2025年07月15日 09:09:00 周二
    }

    @Test
    public void test2(){
        String str = "2025年07月15日 09:09:00 周二";
        SimpleDateFormat sf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss E");
        try {
            //把字符串对象转为Date对象
            Date date = sf.parse(str);
            System.out.println("解析成功:" + date);
        } catch (ParseException e) {
            System.out.println("解析失败");
        }
    }
}

2.2 第 2 代

1、java.util.TimeZone

TimeZone:用于表示时区偏移量。通常,要么使用 getDefault 获取 TimeZone,要么可以用 getTimeZone 及时区 ID 获取 TimeZone

java
package com.atguigu.date;

import org.junit.jupiter.api.Test;

import java.util.TimeZone;

public class TestTimeZone {
    @Test
    public void test1(){
        TimeZone z = TimeZone.getDefault();//默认时区
        System.out.println(z);
        //sun.util.calendar.ZoneInfo[id="Asia/Shanghai"....
    }

    @Test
    public void test2(){
        TimeZone z = TimeZone.getTimeZone("America/Los_Angeles");
        System.out.println(z);
        //sun.util.calendar.ZoneInfo[id="America/Los_Angeles"...
    }

    @Test
    public void test3(){
        String[] all = TimeZone.getAvailableIDs();
        for (String id : all) {
            System.out.println(id);
        }
    }
}

2、java.util.Locale

Locale 对象表示了特定的地理、政治和文化地区。

java
package com.atguigu.date;

import org.junit.jupiter.api.Test;

import java.util.Locale;

public class TestLocale {
    @Test
    public void test1(){
        /*
        以下是几个常见的 ISO 语言代码(ISO 639-1 标准):
        en:英语(English)
        zh:中文(Chinese)
        es:西班牙语(Spanish)
        fr:法语(French)
        de:德语(German)
        ja:日语(Japanese)
        ko:韩语(Korean)
        ru:俄语(Russian)
         */
        Locale t = new Locale("zh");
        System.out.println(t);
    }

    @Test
    public void test2(){
        Locale china = Locale.CHINA;
        System.out.println(china);
    }
}

3、java.util.Calendar

Calendar 类是一个抽象类,它为特定瞬间与一组诸如 YEARMONTHDAY_OF_MONTHHOUR[日历字段](../../java/util/Calendar.html#fields)之间的转换提供了一些方法,并为操作日历字段(例如获得下星期的日期)提供了一些方法。Calendar 提供了一个类方法 getInstance,以获得此类型的一个通用的对象。

java
package com.atguigu.date;

import org.junit.jupiter.api.Test;

import java.util.Calendar;
import java.util.Locale;
import java.util.TimeZone;

public class TestCalendar {
    @Test
    public void test1(){
        Calendar c = Calendar.getInstance();//获取默认的系统是日期时间,以当前运行环境的时区和语言环境来获取日历对象
        System.out.println(c);
        /*
        java.util.GregorianCalendar[time=1752542624606,areFieldsSet=true,areAllFieldsSet=true,lenient=true,
        zone=sun.util.calendar.ZoneInfo[id="Asia/Shanghai",offset=28800000,dstSavings=0,useDaylight=false,transitions=31,lastRule=null],
        firstDayOfWeek=1,minimalDaysInFirstWeek=1,ERA=1,
        YEAR=2025,MONTH=6,WEEK_OF_YEAR=29,WEEK_OF_MONTH=3,DAY_OF_MONTH=15,DAY_OF_YEAR=196,
        DAY_OF_WEEK=3,DAY_OF_WEEK_IN_MONTH=3,AM_PM=0,HOUR=9,HOUR_OF_DAY=9,MINUTE=23,SECOND=44,
        MILLISECOND=606,ZONE_OFFSET=28800000,DST_OFFSET=0]
         */

        int year = c.get(Calendar.YEAR);
        int month = c.get(Calendar.MONTH) + 1;
        int day = c.get(Calendar.DAY_OF_MONTH);

        int hour = c.get(Calendar.HOUR_OF_DAY);
        int minute = c.get(Calendar.MINUTE);
        System.out.println(year +"年" + month + "月" + day + "日 " + hour + ":" + minute);
    }

    @Test
    public void test2(){
        TimeZone zone = TimeZone.getTimeZone("America/Los_Angeles");
        Locale locale = Locale.US;
        Calendar c = Calendar.getInstance(zone,locale);
        int year = c.get(Calendar.YEAR);
        int month = c.get(Calendar.MONTH) + 1;
        int day = c.get(Calendar.DAY_OF_MONTH);

        int hour = c.get(Calendar.HOUR_OF_DAY);
        int minute = c.get(Calendar.MINUTE);
        System.out.println(year +"年" + month + "月" + day + "日 " + hour + ":" + minute);
        //2025年7月14日 18:28
    }
}

2.3 第 3 代

Java8 版本引入了第 3 代的日期时间 API。它们主要在 java.time 及其子包。

1、本地日期或时间(最多)

  • LocalDate
  • LocalTime
  • LocalDateTime
java
package com.atguigu.date;

import org.junit.jupiter.api.Test;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;

public class TestLocalDateTime {
    @Test
    public void test1(){
        //本地日期
        LocalDate ld = LocalDate.now();
        System.out.println(ld);
        //2025-07-15
    }

    @Test
    public void test2(){
        LocalDate ld = LocalDate.of(1999, 7, 15);
        System.out.println(ld);
    }

    @Test
    public void test3(){
        //本地时间
        LocalTime lt = LocalTime.now();
        System.out.println(lt);
        //09:32:24.162140300
    }

    @Test
    public void test4(){
        LocalTime lt = LocalTime.of(9, 50, 00);
        System.out.println(lt);//09:50
    }

    @Test
    public void test5(){
        //本地日期+时间
        LocalDateTime ldt = LocalDateTime.now();
        System.out.println(ldt);

        LocalDateTime dateTime = LocalDateTime.of(2025, 7, 15, 9, 50);
        System.out.println(dateTime);
        /*
        2025-07-15T09:33:41.323818300
        2025-07-15T09:50
         */
    }
}

2、其他地区的日期时间

  • ZoneId:代替原来的 TimeZone
  • ZonedDateTime:代替原来的 Calendar
java
package com.atguigu.date;

import org.junit.jupiter.api.Test;

import java.time.ZoneId;
import java.time.ZonedDateTime;

public class TestZonedDateTime {
    @Test
    public void test1(){
        ZonedDateTime zdt = ZonedDateTime.now();
        System.out.println(zdt);
        //2025-07-15T09:36:32.291563700+08:00[Asia/Shanghai]

        ZoneId id = ZoneId.of("America/Los_Angeles");;
        ZonedDateTime other = ZonedDateTime.now(id);
        System.out.println(other);
        //2025-07-14T18:36:32.292560500-07:00[America/Los_Angeles]
    }

    @Test
    public void test2(){
        ZonedDateTime zdt = ZonedDateTime.of(2025, 7, 15, 9, 36, 32, 291563700, ZoneId.of("Asia/Shanghai"));
        System.out.println(zdt );

        ZonedDateTime zdt1 = ZonedDateTime.of(2025, 7, 15, 9, 36, 32, 291563700, ZoneId.of("America/Los_Angeles"));
        System.out.println(zdt1);
    }
}

3、Instant

java
package com.atguigu.date;

import org.junit.jupiter.api.Test;

import java.time.Instant;
import java.time.ZonedDateTime;

public class TestInstant {
    @Test
    public void test1(){
        Instant now = Instant.now();//本初子午线的时间
        System.out.println(now );
        //2025-07-15T01:38:49.992202700Z
    }

    @Test
    public void test2(){
        ZonedDateTime zdt = ZonedDateTime.now();
        System.out.println(zdt);
        //2025-07-15T09:36:32.291563700+08:00[Asia/Shanghai]
    }
}

4、一段日期和一段时间

  • Period:日期间隔
  • Duration:时间间隔
java
package com.atguigu.date;

import org.junit.jupiter.api.Test;

import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.Period;

public class TestPeriod {
    @Test
    public void test(){
        LocalDate today = LocalDate.now();
        LocalDate start = LocalDate.of(2025, 6, 25);
        Period period = Period.between(start, today);
        System.out.println(period);
        //P20D
    }

    @Test
    public void test2(){
        LocalDate today = LocalDate.now();
        LocalDate birthday = LocalDate.of(2000, 5, 1);
        Period period = Period.between(birthday, today);
        System.out.println(period);
        //P25Y2M14D
    }

    @Test
    public void test3(){
        LocalTime now = LocalTime.now();
        LocalTime time = LocalTime.of(9, 50, 0);
        Duration duration = Duration.between(now, time);
        System.out.println(duration);//PT6M39.935055S
    }
}

5、格式化

  • DateTimeFormatter:代替原来的 SimpleDateFormat
  • FormatStyle:是一个枚举类,有几种预定义的风格
java
package com.atguigu.date;

import org.junit.jupiter.api.Test;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;

public class TestDateTimeFormatter {
    @Test
    public void test1(){
        LocalDateTime ldt = LocalDateTime.now();
        System.out.println(ldt);
        //2025-07-15T09:45:28.725312100

        //希望显示为:2025年07月15日 09:06:56 星期几
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss E");
        String str = dtf.format(ldt);
        System.out.println(str);
        //2025年07月15日 09:46:20 周二
    }

    @Test
    public void test2(){
        LocalDateTime ldt = LocalDateTime.now();
        DateTimeFormatter dt = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL).withZone(ZoneId.of("Asia/Shanghai"));
        String str = dt.format(ldt);
        System.out.println(str);
        //2025年7月15日星期二 中国标准时间 上午9:48:54
    }

    @Test
    public void test3(){
        LocalDateTime ldt = LocalDateTime.now();
        DateTimeFormatter dt = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG).withZone(ZoneId.of("Asia/Shanghai"));
        String str = dt.format(ldt);
        System.out.println(str);
        //2025年7月15日 CST 上午9:49:17
    }

    @Test
    public void test4(){
        LocalDateTime ldt = LocalDateTime.now();
        DateTimeFormatter dt = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withZone(ZoneId.of("Asia/Shanghai"));
        String str = dt.format(ldt);
        System.out.println(str);
        //2025年7月15日 上午9:49:38
    }

    @Test
    public void test5(){
        LocalDateTime ldt = LocalDateTime.now();
        DateTimeFormatter dt = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT).withZone(ZoneId.of("Asia/Shanghai"));
        String str = dt.format(ldt);
        System.out.println(str);
        //2025/7/15 上午9:49
    }
}

2.4 三代转换问题

情况一:第 1 代和第 2 代的转换

java
package com.atguigu.date;

import org.junit.jupiter.api.Test;

import java.util.Calendar;
import java.util.Date;

public class TestOneTwo {
    @Test
    public void test(){
        Date d = new Date();
        Calendar c = Calendar.getInstance();
        c.setTime(d);//设置时间为 d对象的时间
        int year = c.get(Calendar.YEAR);
        int month = c.get(Calendar.MONTH) + 1;
        int day = c.get(Calendar.DAY_OF_MONTH);

        int hour = c.get(Calendar.HOUR_OF_DAY);
        int minute = c.get(Calendar.MINUTE);
        System.out.println(year +"年" + month + "月" + day + "日 " + hour + ":" + minute);
    }

    @Test
    public void test2(){
        Calendar c = Calendar.getInstance();
        Date d = c.getTime();
        System.out.println(d);
        //Tue Jul 15 10:09:35 CST 2025
    }
}

情况二:第 1 代与第 3 代

java
package com.atguigu.date;

import org.junit.jupiter.api.Test;

import java.time.*;
import java.util.Date;

public class TestOneThree {
    @Test
    public void test1(){
        Date d = new Date();
        Instant instant = d.toInstant();//第1代->第3代
        LocalDateTime ldt = LocalDateTime.ofInstant(instant, ZoneId.of("Asia/Shanghai"));
        System.out.println(ldt);
        //2025-07-15T10:12:03.075
    }

    @Test
    public void test2(){
        LocalDateTime ldt = LocalDateTime.now();
        ZonedDateTime zonedDateTime = ldt.atZone(ZoneId.of("Asia/Shanghai"));//通过时区id创建时区
        Instant instant = zonedDateTime.toInstant();
        Date date = Date.from(instant);
        System.out.println(date);
        //Tue Jul 15 10:13:55 CST 2025
    }

    @Test
    public void test3(){
        LocalDateTime ldt = LocalDateTime.now();
        Instant instant = ldt.toInstant(ZoneOffset.of("+08:00"));//东八区,偏移8个小时的时区
        Date date = Date.from(instant);
        System.out.println(date);
        //Tue Jul 15 10:15:49 CST 2025
    }
}

2.5 小结说明

建议使用第 3 代,因为第 1 代和第 2 代有一些问题:

  • 麻烦
  • 线程安全问题(等讲完线程安全部分再回来看)
  • 对象的可变性问题
  • 闰秒问题
java
package com.atguigu.date;

import org.junit.jupiter.api.Test;

import java.time.LocalDate;
import java.util.Calendar;

public class TestProblem {
    @Test
    public void test1(){
        Calendar c = Calendar.getInstance();
        c.set(Calendar.YEAR, 2056);
        int year = c.get(Calendar.YEAR);
        int month = c.get(Calendar.MONTH) + 1;
        int day = c.get(Calendar.DAY_OF_MONTH);

        int hour = c.get(Calendar.HOUR_OF_DAY);
        int minute = c.get(Calendar.MINUTE);
        System.out.println(year +"年" + month + "月" + day + "日 " + hour + ":" + minute);

    }

    @Test
    public void test2(){
        LocalDate today = LocalDate.now();
        System.out.println("today = " + today);

        today.plusDays(100);//非正确使用方式
        System.out.println("today = " + today);

        LocalDate newDate = today.plusDays(100);//不会对原对象进行修改,而是返回新对象
        System.out.println("newDate = " + newDate);
    }
}

三、字符串(非常重要)

3.1 可变字符串和不可变字符串

它们都是 CharSequence 的实现类。

  • 不可变字符串:String
    • 内部的 value 数组是 final 修饰,意味着我们不可以修改 value 数组的引用(即同一个字符串对象来说 value 不能指向新数组,意味着不能扩容等)
    • 因为 String 不可变,每次修改都会产生新对象,如果出现频繁修改字符串的时候,效率就比较低。
  • 可变字符串:StringBuffer 和 StringBuider
    • 内部的 value 数组没有 final 修饰,意味着 value 可以指向新数组,即可以扩容
    • StringBuffer 和 StringBuider 的方法完全相同,差别在于 StringBuffer 的方法有 synchronized(同步锁),StringBuider 的方法没有 synchronized(同步锁)
    • 当没有多线程的场景,即不存在安全隐患的情况下,优先使用 StringBuider,因为通常它的效率更高

面试题 1:String 与 StringBuffer 和 StringBuider 区别

  • String :字符串对象不可变
  • StringBuffer 和 StringBuider :字符串对象可变

面试题 2:StringBuffer 和 StringBuider 区别

StringBuffer:旧的(JDK1.0),线程安全的,效率低,所有操作字符串的方法有 synchronized(同步锁)

StringBuider :新的(JDK1.5),线程不安全的,效率高,它的方法没有 synchronized(同步锁)

后面还会正式学习线程安全问题,先简单解释一下:

多个线程同时操作同一个字符串的时候,比喻 多个人同时要使用同一个 卫生间(就一个马桶),如果没有协调,就会有尴尬的场景。

线程的安全就是保证同一个时刻只能有 1 个人用厕所,门上会有锁,当使用卫生间的人上锁之后,其他人只能等待。

线程不安全的意思:大家就乱用

3.2 StringBuffer 和 StringBuider 的 API

StringBuffer 和 StringBuider 它们又被称为字符串缓冲区,它们内部的 value 数组相当于一个缓冲区,默认大小 16。如果缓冲区大小写不够,会自动扩容,扩容的机制是 2 倍 + 2。

为什么是 2 倍 + 2?

因为 StringBuffer 和 StringBuider 有一个构造器,可以手动指定初始化容量,

即 StringBuffer b = new StringBuffer(0); //此时内部的 value 数组长度就为 0

如果是 2 倍扩容, 2*0 = 0 ,就无法实现扩容。

如果是 2 倍 + 2 扩容,2*0 + 2 = 2,可以扩容。

围绕增删改查:

1、增加

  • append:末尾追加
  • insert:插入

2、删除

  • deleteCharAt(offset):删除[offset]位置的 1 个字符
  • delete(start, end):删除[start, end) 范围的字符

3、修改

  • setCharAt(下标,新字符):修改[下标]位置的 1 个字符
  • setLength(新长度):修改长度
  • reverse():反转
  • replace(start, end , 新子串):替换[start, end)范围的字符

4、查询

  • indexOf(子串):查找子串首次出现的下标
  • lastIndexOf(子串):查找子串末次出现的下标
  • charAt(index):返回[index]位置的 1 个字符
  • substring(start, end):返回[start, end)范围的字符
java
package com.atguigu.string;

import org.junit.jupiter.api.Test;

public class TestStringBuilder {
    @Test
    public void test1(){
        StringBuilder builder = new StringBuilder();
        builder.append("hello");
        builder.append(1);
        builder.append(1.0);
        builder.append(true);
        System.out.println(builder);
    }

    @Test
    public void test2(){
        StringBuilder builder = new StringBuilder("hello");
        builder.insert(3, "java");
        System.out.println(builder);
        //heljavalo
    }

    @Test
    public void test3(){
        StringBuilder builder = new StringBuilder("hellojavaworld");
        builder.deleteCharAt(0);
        System.out.println(builder);//ellojavaworld

        builder.delete(0,3);//删除[0,3)
        System.out.println(builder);//ojavaworld
    }

    @Test
    public void test4(){
        StringBuilder builder = new StringBuilder("hellojavaworld");
        builder.setCharAt(0,'H');//首字母改为大写
        System.out.println(builder);//Hellojavaworld
        builder.reverse();//翻转
        System.out.println(builder);//dlrowavajolleH
        builder.replace(0,3,"尚硅谷");
        System.out.println(builder);
        builder.setLength(5);
        System.out.println(builder);//尚硅谷ow

        builder.setLength(20);
        System.out.println(builder);//尚硅谷ow
    }

    @Test
    public void test5(){
        StringBuilder builder = new StringBuilder("hellojavaworldjavajava");
        int index = builder.indexOf("java");
        System.out.println(index);//5

        int lastIndex = builder.lastIndexOf("java");
        System.out.println(lastIndex);//18

        char c = builder.charAt(1);
        System.out.println(c);//e

        String sub = builder.substring(0, 3);
        System.out.println(sub);//hel
    }

}

3.3 String 的特点

1、String 类型的对象不可变。

保证它的对象不可变的设计有哪些?

  • value 数组前面加 final,final 只能保证 value 不指向新数组,长度不可变
  • value 前面有 private,意味着外部无法直接操作 value 数组。然后在 String 类的所有方法,只要涉及到修改 value 数组的元素内容的,它通通都给你返回一个新 String 对象

2、String 类本身是 final,即 String 类没有子类

3、因为 String 类的对象不可变,所以部分字符串对象就可以被共享。被放到字符串常量池中的字符串对象会被共享。只有 2 种字符串对象会被放到字符串常量池中:

  • 直接"" 引起来的字符串
  • 字符串对象调用intern()方法的结果

建议不是特别的需求,使用字符串就用"",可以共享对象,从而节约内存。

image-20250715115945735

内存区域JDK7JDK8
方法区- 实现为永久代 - 类信息(类元数据) - 运行时常量池 - JIT 编译后的代码 - 静态变量-实现为元空间,位于本地内存(Native Memory) - 类信息(类元数据) - 运行时常量池 - JIT 编译后的代码
- 对象实例(包括数组) - 字符串常量池(JDK7 从永久代移至堆)- 对象实例(包括数组) - 字符串常量池 - 静态变量(JDK8 从永久代移至堆)
Java 虚拟机栈- 栈帧:局部变量表、操作数栈、动态链接、方法出口与 JDK7 相同
本地方法栈- Native 方法调用的栈帧与 JDK7 相同
程序计数器当前线程执行的字节码指令地址与 JDK7 相同

4、字符串对象个数

java
package com.atguigu.string;

import org.junit.jupiter.api.Test;

public class TestStringCount {
    @Test
    public void test1(){
        String s1 = "hello";
        String s2 = "hello";
        //问:上面有几个字符串对象?
        //2个字符串的变量,指向同一个字符串对象
    }

    @Test
    public void test2(){
        String s3 = new String("hello");
        //问:上面有几个字符串对象?
        //2个。(1)"hello"是一个字符串对象,(2)new了一个
    }

    @Test
    public void test3(){
        String s = "hello" + "world" + "java" + "atguigu";
        //问:上面有几个字符串对象?
        //1个。编译器直接优化处理为 helloworldjavaatguigu";
    }

    @Test
    public void test4(){
        String s1 = "hello";
        String s2 = new String("hello").intern();
        //2个(1)"hello"是一个字符串对象,(2)new了一个
        //intern() 方法会先去常量池中寻找是否已经有 "hello",如果有直接返回常量池中那个字符串的地址,
        // 如果没有则将 "hello" 放入常量池中,并返回
        System.out.println(s1 == s2);//true
    }
}

image-20250715154051738

image-20250715154104135

5、字符串内部的 value 数组是什么类型的?

  • JDK9(不含)之前:char[]
  • JDK9 之后:byte[]

问 1:为什么要从 char[]转为 byte[]?

(1)对于大部分字符串来说,都只是包含 ASCII 表范围内的字符,这些字符的编码值是[0, 127]范围,实际上它们只需要 1 个字节就可以存储。

那么如果使用 char[],会有一半内存是浪费的,因为一个 char 要占 2 个字节。

对于非 ASCII 表范围内的字符,例如中文等字符,那么 1 个 byte 存不下它们的编码值,仍然需要 2 个或更多个字节。

(2)char 是 2 个字节,1 个 char 最多可以表示的编码值范围是[0, 65535],如果使用 char,就无法表示 emoji 表情等字符,因为这些字符的 Unicode 编码值超过 [0, 65535],它们需要更多字节来表示。

问 1:改为 byte[]数组之后,如果一个字符串中全是 ASCII 表范围内的字符,1 个字符占几个字节;如果一个字符串中有 ASCII 表范围内的字符和其他字符共同组成,1 个字符占几个字节?

image-20250715143537524

3.4 String 的方法

  • int length():字符串的长度
  • char charAt(下标):返回[下标]位置的字符
  • int indexOf(字符或子串):返回字符或子串的首次出现下标
  • int lastIndexOf(字符或子串):返回字符或子串的末次出现下标
  • boolean contains(子串):是否包含子串部分
  • boolean isEmpty():是否不包含任何字符,即字符串长度为 0
  • boolean isBlank():是否不包含空格、\t、\n 等空白字符以外的字符
  • String toUpperCase():转大写
  • String toLowerCase():转小写
  • boolean endsWith(xx):是否以 xx 结尾
  • boolean startsWith(xx):是否以 xx 开头
  • String substring(下标):从[下标]开始截取到最后
  • String substring(start, end):截取[start, end)部分
  • boolean equals(Object obj):两个字符串内容是否完全相同(区分大小写)
  • boolean equalsIgnoreCase(另一个字符串):两个字符串内容是否相同(不区分大小写)
  • int compareTo(另一个字符串):比较两个字符串大小,区分大小写
  • int compareToIgnoreCase(另一个字符串):比较两个字符串大小,不区分大小写
  • String concat(另一个字符串):字符串拼接
  • String trim():去掉前后空格
  • char[] toCharArray():把字符串转为 char[]数组
  • String valueOf(char[]):把 char[]数组转为字符串
  • byte[] getBytes(【编码方式】):加密的过程
  • new String(byte[] ,【编码方式】):解密的过程
java
package com.atguigu.string;

import org.junit.jupiter.api.Test;

import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Scanner;

public class TestStringMethod {
    @Test
    public void test1(){
        String str = "hello中";
        System.out.println("字符的个数或字符串长度:" + str.length());//6
        //与字符串底层占的字节数无关。单纯的数字符的个数
        System.out.println("取[下标]位置的字符:" + str.charAt(5));//中
        //无论底层是byte[],还是char[],以及1个字符占几个字节,它的这个下标是从第几个字符的角度来说,从0开始
        System.out.println(str.indexOf('l'));//2   indexOf找xx的下标
        System.out.println(str.lastIndexOf('l'));//3  lastIndexOf找xx的末次下标
        System.out.println(str.contains("wo"));//false   contains是否包含
        System.out.println(str.contains("he"));//true
    }

    @Test
    public void test2(){
        String str = "  ";
        System.out.println("是否为空?" + str.isEmpty());//false
        System.out.println("是否为空?" + str.isBlank());//true

        String str2 = "";
        System.out.println("是否为空?" + str2.isEmpty());//true
        System.out.println("是否为空?" + str2.isBlank());//true
        /*
        isEmpty(),不包含任何字符
        isBlank(),除了空白字符,不包含其他字符。空白字符有空格、\t、\n等
         */
    }

    @Test
    public void test3(){
        String str = "Hello中";
        System.out.println("转大写:" + str.toUpperCase());
        System.out.println("转小写:" + str.toLowerCase());
        /*
        转大写:HELLO中
        转小写:hello中
         */
    }

    @Test
    public void test4(){
        String str = "Hello.java";
        //这个字符串是否以.java结尾
        System.out.println("是否以.java结尾:" + str.endsWith(".java"));
        String name = "张三";
        //这个字符串是不是以张开头
        System.out.println("是不是以张开头:" + str.startsWith("张") );
    }

    @Test
    public void test5(){
        String filename = "Hello.java";
        //截取上述文件名中的 后缀名
        int index = filename.lastIndexOf(".");
        String ext = filename.substring(index);//从[index]开始截取到最后
        System.out.println(ext);

        //截取上述文件名中的 除去后缀名之外部分,例如:Hello
        String name = filename.substring(0, index);//截取[0, index)部分
        System.out.println(name);
    }

    @Test
    public void test6(){
        //模拟登录:  假设数据库中的用户名是 atguigu,密码是 123456,验证码:tAb8
        Scanner input = new Scanner(System.in);

        System.out.print("请输入验证码:");
        String code = input.next();

        /*
        解决验证码不区分大小写比较问题的思路有2种:
        (1)if(code.toLowerCase().equals("tAb8".toLowerCase()))
        (2) 调用字符串的 equalsIgnoreCase 方法,忽略大小写比较字符串内容
         */
        if(code.equalsIgnoreCase("tAb8")){
            System.out.println("验证码正确");
        }else{
            System.out.println("验证码不正确");
            return;
        }

        System.out.print("请输入用户名:");
        String username = input.next();

        System.out.print("请输入密码:");
        String password = input.next();

        //input.next()得到的字符串,不是用""直接得到的字符串,因此它们不是字符串常量,不能共享,所以不能用==比较
        /*if(username == "atguigu" && password == "123456"){
            System.out.println("登录成功");
        }else{
            System.out.println("登录失败");
        }*/

        //String已经重写了Object类的方法,我们直接用,它重写为比较字符串的内容,而不是地址
        //equals是区分大小写
        if(username.equals("atguigu") && password.equals("123456")){
            System.out.println("登录成功");
        }else{
            System.out.println("登录失败");
        }

        input.close();
    }

    @Test
    public void test7(){
        String s1 = "hello";
        String s2 = "world";
//        System.out.println(s1 > s2);//错误。 s1,s2是对象,s1和s2是地址值,是无法直接用运算符比较大小的

        int result = s1.compareTo(s2);//因为String类实现了Comparable接口,重写了compareTo方法
        System.out.println(result);//-15  负整数代表 s1小于s2
    }

    @Test
    public void test8(){
        String s1 = "hello";
        String s2 = "Hello";
        int result = s1.compareTo(s2);//因为String类实现了Comparable接口,重写了compareTo方法 区分大小写
        System.out.println(result);//32  正整数代表 s1大于s2
    }

    @Test
    public void test9(){
        String[] strings = {"hello","java","chai","Hi","Tom"};
        //自然排序
        Arrays.sort(strings);  //依赖于String类实现 Comparable接口,重写了compareTo方法 区分大小写
        System.out.println(Arrays.toString(strings));
        //[Hi, Tom, chai, hello, java]
    }

    @Test
    public void test10(){
        String[] strings = {"hello","java","chai","Hi","Tom"};
        //定制排序  找Comparator接口
        Comparator c = new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                String s1 = (String) o1;
                String s2 = (String) o2;
                return s1.compareToIgnoreCase(s2);//忽略大小写比较字符串大小
            }
        };

        Arrays.sort(strings, c);
        System.out.println(Arrays.toString(strings));
        //[chai, hello, Hi, java, Tom]
    }

    @Test
    public void test11(){
        String str = "  he   llo   ";
        str = str.trim();
        System.out.println("[" + str +"]");
    }

    @Test
    public void test12(){
        String str1 = "hello";
        String str2 = str1.trim();//因为"hello"前后没有空白字符,所以字符串不需要处理,会返回原字符串对象的地址
        System.out.println(str1 == str2);//true
    }

    @Test
    public void test13(){
        String str1 = "hello ";
        String str2 = str1.trim();//因为"hello"前后有空白字符,需要处理,就会返回新字符串对象
        System.out.println(str1 == str2);//false
    }

    @Test
    public void test14(){
        char[] arr = {'h', 'e', 'l', 'l', 'o','w','o'};
        //可以根据char[]构建字符串
        String str = new String(arr);
        String str2 = String.valueOf(arr);
        System.out.println(str);
        System.out.println(str2);

        String str3 = new String(arr, 1,4);
        String str4 = String.valueOf(arr, 1,4);
        System.out.println(str3);
        System.out.println(str4);
    }

    @Test
    public void test15(){
        String str = "hello";
        char[] arr = str.toCharArray();
        System.out.println(str);
        System.out.println(arr);//char[]比较特殊,直接输出,会打印出数组内容,而不是地址值
    }

    @Test
    public void test16(){
        String str = "abcd";
        byte[] bytes = str.getBytes();//按照当前运行环境的编码来对字符串进行编码
        System.out.println(Arrays.toString(bytes));//[97, 98, 99, 100]
    }

    @Test
    public void test17(){
        String str = "abcd中";
        byte[] bytes = str.getBytes();//按照当前运行环境的编码来对字符串进行编码
        System.out.println(Arrays.toString(bytes));//[97, 98, 99, 100, -28, -72, -83]
        //默认咱们的idea编码设置的是UTF-8,那么在UTF-8编码一个中文字符占3个字节
    }

    @Test
    public void test18(){
        String str = "abcd中";
        byte[] bytes = str.getBytes(Charset.forName("GBK"));//加密的过程
        System.out.println(Arrays.toString(bytes));//[97, 98, 99, 100, -42, -48]
        //在GBK编码规则中,一个汉字占2个字节
        /*
        比喻:编码方式或编码表 相当于  对情报加密的密码本
        同一个情报,用不同的密码本加密得到的密文是不同的。

        加密的密码本和解码的密码本必须相同,才能解密,否则会乱码
         */
    }

    @Test
    public void test19(){
        byte[] bytes = {97, 98, 99, 100, -42, -48};
        String str = new String(bytes); //解密  默认是UTF-8(因为IDEA的编码是UTF-8)
        System.out.println(str);//abcd�� 乱码
    }

    @Test
    public void test20(){
        byte[] bytes = {97, 98, 99, 100, -42, -48};
        String str = new String(bytes,Charset.forName("GBK")); //解密  指定GBK
        System.out.println(str);//abcd中
    }

    @Test
    public void test21(){
        String s1 = "hello";
        s1 = s1 + "world";//拼接

        s1 = s1.concat("java");//拼接
        System.out.println(s1);
    }
}

四、正则表达式

4.1 什么是正则表达式

所谓正则表达式,定义某一个字符串/某一段文本的规则的表达式。例如:手机号码有规则,注册一些网站的用户名或密码有规则等。

4.2 定义正则表达式用到的一些特定字符

1、元字符

(1)字符类

[abc]abc(简单类)

[^abc]:任何字符,除了 abc(否定)

[a-zA-Z]azAZ,两头的字母包括在内(范围)

(2)预定义字符类

.:任何字符(与行结束符可能匹配也可能不匹配)

\d:数字:[0-9]

\D:非数字: [^0-9]

\s:空白字符:[ \t\r\n\f\x0B],例如:空格,制表位\t,回车\r,换行\n,换页\f,竖向制表符\x0B

\S:非空白字符:[^\s]

\w:单词字符:[a-zA-Z_0-9],例如:大小写字母,数字,下划线

\W:非单词字符:[^\w]

(3)POSIX 字符类(仅 US-ASCII)

\p{Lower} 小写字母字符:[a-z]

\p{Upper} 大写字母字符:[A-Z]

\p{ASCII} 所有 ASCII:[\x00-\x7F]

\p{Alpha} 字母字符:[\p{Lower}\p{Upper}]

\p{Digit} 十进制数字:[0-9]

\p{Alnum} 字母数字字符:[\p{Alpha}\p{Digit}]

\p{Punct} 标点符号:!"#$%&'()*+,-./:;<=>?@[]^_`{|}~

\p{Blank} 空格或制表符:[ \t]

\p{Graph} 可见字符:[\p{Alnum}\p{Punct}],字母,数字,标点符号

\p{Print} 可打印字符:[\p{Graph}\x20],例如:字母,数字,标点符号,空格

2、Greedy 数量词

X?X,一次或一次也没有

X*X,零次或多次

X+X,一次或多次

X{n}X,恰好 n

X{n,}X,至少 n

X{n,m}X,至少 n 次,但是不超过 m

3、边界匹配器

^:行的开头

$:行的结尾

\b:单词边界

\B:非单词边界

4、Logical 运算符

XYX 后跟 Y

X|YXY

5、捕获组和非捕获组

(1)捕获组

(X):X,作为捕获组

(2)特殊构造(非捕获组)

(?:X) :X,作为非捕获组

(?>X): X,作为独立的非捕获组

(?=X) :X,通过零宽度的正 lookahead

(?<=X) :X,通过零宽度的正 lookbehind

(?!X) :X,通过零宽度的负 lookahead

(?<!X) :X,通过零宽度的负 lookbehind

4.3 案例演示

4.3.1 String 类相关的方法

  • boolean matches(正则):判断字符串是否满足 xx 规则,满足返回 true,否则返回 false
  • String replace(旧字符,新字符):不支持正则,
  • String replaceFirst(正则,新子串):替换第一个满足正则的字符为新子串
  • String replaceAll(正则,新子串):替换所有满足正则的字符为新子串
  • String[] split(正则):按照正则对字符串进行拆分
java
package com.atguigu.regular;

import org.junit.jupiter.api.Test;

import java.util.Arrays;

public class TestString {
    @Test
    public void test1(){
        //简单判断是否全部是数字,这个数字可以是1~n位
        String str = "12345";
        boolean result = str.matches("\\d+");
        System.out.println(result);
    }

    @Test
    public void test2(){
        String str = "111456789";
        //判断它是否全部由数字组成,并且第1位不能是0,长度为9位
        boolean result = str.matches("[1-9]\\d{8}");
        System.out.println("result = " + result);
    }

    @Test
    public void test3(){
        //密码要求:必须有大写字母,小写字母,数字组成,6位
        String str = "Cly892";
//        String str = "clycly";
        boolean result = str.matches("^(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])[A-Za-z0-9]{6}$");
        //.代表任意字符,*代表任意次
        //(?=.*[A-Z]) 必须在字符串中找到大写字母,继续看其他规则,否则就返回false
        //(?=.*[a-z]) 必须在字符串中找到小写字母,继续看其他规则,否则就返回false
        //(?=.*[0-9]) 必须在字符串中找到数字,继续看其他规则,否则就返回false
        //[A-Za-z0-9]{6} 长度为0,而且全部由大小写字母和数字组成,没有其他字符出现
        System.out.println(result);
    }

    @Test
    public void test4(){
        String str = "atguigu666hello244world.java;887ai888尚硅谷";

        //删除其中的数字244
        String str1 = str.replace("244", "");
        System.out.println(str1);
        //atguigu666helloworld.java;887ai888


        //删除第1个数字
        String str2 = str.replaceFirst("\\d+", "");
        System.out.println(str2);

        //删除所有数字
        String str3 = str.replaceAll("\\d+", "");
        System.out.println(str3);


        //把其中的非字母去掉
        String str4 = str.replaceAll("[^a-zA-Z]", "");
        System.out.println(str4);


        System.out.println(str);//不变
    }

    @Test
    public void test5(){
        String str = "Hello World java atguigu";
        //按照空格拆分
        String[] strings = str.split(" ");
        for (String s : strings) {
            System.out.println(s);
        }
    }

    @Test
    public void test6(){
        String str = "Hello2World333java4atguigu";
        //按照数字进行拆分
        str = str.replaceFirst("^\\d+","");
        //^\d+ 代表开头的数字
        System.out.println(str);
        String[] strings = str.split("\\d+");
        System.out.println(Arrays.toString(strings));
        for (String s : strings) {
            System.out.println(s);
        }
    }

    @Test
    public void test7(){
        String str = "张三.23|李四.24|王五.25";
        //按照|进行拆分为多个学生信息,获取每个学生的姓名和年龄按照.进行拆分,构建Student对象
        String[] strings = str.split("\\|");
        Student[] all = new Student[strings.length];
        for (int i = 0; i < strings.length; i++) {
            String[] nameAgeStr = strings[i].split("\\.");
            /*
            nameAgeStr[0] 是姓名
            nameAgeStr[1] 是年龄
             */
            System.out.println(Arrays.toString(nameAgeStr));
            String name = nameAgeStr[0];
            int age = Integer.parseInt(nameAgeStr[1]);
            all[i] = new Student(name, age);
        }
        System.out.println("学生对象数组:");
        for (Student s : all) {
            System.out.println(s);
        }
    }
}
java
package com.atguigu.regular;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
    private String name;
    private int age;
}

4.3.2 Pattern 和 Matcher 类

Pattern 是正则表达式的编译表示形式。 正则表达式必须首先被编译为此类的实例,才能将得到的模式用于创建 Matcher 对象。

Matcher 对象可以依照正则表达式与任意字符序列匹配。

1、捕获组的提取与反向引用

java
package com.atguigu.regular;

import org.junit.jupiter.api.Test;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class TestGroup {
    @Test
    public void test1(){
        //从以下身份证号码中提取出生日期
        String cardId = "110531199605026932,441531201205013932,330881200012069635";
        //6位代表省份地区等信息,8位代表出生的年月日,其中年占4位,月占2为,日占2位,剩下的4位代表性别等信息
        String reg = "\\d{6}((\\d{4})(\\d{2})(\\d{2}))\\d{4}";
        //对正则表达式进行分组,称为捕获组
        //从左往右数,第一个(代表第1组,第二个(代表第2组....
        //第1组 ((\d{4})(\d{2})(\d{2}))
        //第2组 (\d{4})
        //第3组 (\d{2})
        //第4组 (\d{2})
        Pattern pattern = Pattern.compile(reg);//把正则表达式编译为Pattern的对象
        Matcher matcher = pattern.matcher(cardId);
        while(matcher.find()){
            System.out.println("年月日:" + matcher.group(1));
            System.out.println("年:" + matcher.group(2));
            System.out.println("月:" + matcher.group(3));
            System.out.println("日:" + matcher.group(4));
        }
    }

    @Test
    public void test2(){
        //从以下身份证号码中提取出生日期,可以给捕获组取名字
        String cardId = "110531199605026932,441531201205013932,330881200012069635";
        String reg = "\\d{6}(?<birthday>(?<year>\\d{4})(?<month>\\d{2})(?<day>\\d{2}))\\d{4}";
        //对正则表达式进行分组,称为捕获组
        //从左往右数,第一个(代表第1组,第二个(代表第2组....
        //第1组 (?<birthday>(?<year>\d{4})(?<month>\d{2})(?<day>\d{2}))  组名是 birthday
        //第2组 (?<year>\d{4})  组名是year
        //第3组 (?<month>\d{2}) 组名是month
        //第4组 (?<day>\d{2}) 组名是day
        Pattern pattern = Pattern.compile(reg);//把正则表达式编译为Pattern的对象
        Matcher matcher = pattern.matcher(cardId);
        while(matcher.find()){
            System.out.println("年月日:" + matcher.group("birthday"));
            System.out.println("年:" + matcher.group("year"));
            System.out.println("月:" + matcher.group("month"));
            System.out.println("日:" + matcher.group("day"));
        }
    }

    @Test
    public void test3(){
        //提取货币符号和金额
        String money = "价格:¥96,895,681.82 和 $789.00";
        String reg = "([¥$])([\\d,.]+)";
        Pattern pattern = Pattern.compile(reg);
        Matcher matcher = pattern.matcher(money);
        while (matcher.find()){
            System.out.println("货币符号:" + matcher.group(1));
            System.out.println("金额:" + matcher.group(2));
        }
    }

    @Test
    public void test4(){
        //需求:找出字符串中出现的数字最大的一个, 提示:123连续看成1个数字
        //参考答案是最大数字是789
        String str = "4hello123world56java789atguig9";
        String reg = "(\\d+)";
        Pattern pattern = Pattern.compile(reg);
        Matcher matcher = pattern.matcher(str);
        int max = 0;
        while (matcher.find()){
            String numStr = matcher.group(1);
            int num = Integer.parseInt(numStr);
            if(num >max){
                max = num;
            }
        }
        System.out.println(max);
    }

    @Test
    public void test4_2(){
        //需求:找出字符串中出现的数字最大的一个, 提示:123连续看成1个数字
        //参考答案是最大数字是789
        String str = "4hello123world56java789atguig9";
        String[] strings = str.split("[^\\d]+");
        int max = 0;
        for (String string : strings) {
            int num = Integer.parseInt(string);
            if(num > max){
                max = num;
            }
        }
        System.out.println("max = " + max);
    }


    @Test
    public void test5(){
        //找出AABB式的成语
        String str = "七七八八、三心二意、马马虎虎、高高在上、风风雨雨、大腹便便";
        String reg = "(.)\\1(.)\\2";//反向引用捕获组的内容
        Pattern pattern = Pattern.compile(reg);
        Matcher matcher = pattern.matcher(str);
        while(matcher.find()){
            String s = matcher.group();
            System.out.println(s);
        }
    }

    @Test
    public void test6(){
        //找出开头的1个字母和结尾1个字母相同的单词,不管中间是什么字符。
        String str = "hello,level,ok,noon,world,high,local,nineteen";
        String reg = "\\b([a-z])[a-z]*\\1";
        //\b代表单词边界
        Pattern pattern = Pattern.compile(reg);
        Matcher matcher = pattern.matcher(str);
        while(matcher.find()){
            String s = matcher.group();
            System.out.println(s);
        }
    }

    @Test
    public void test7(){
        //去除连续重复的字,例如:我我我改为我
        //参考答案:我爱尚硅谷
        String str = "我我我爱爱爱爱尚尚尚硅谷";
        String reg = "(.)\\1+";
        /*
        在正则里面反向引用捕获组,用\组编号
        在正则外面使用捕获组,用$组编号
         */
        str = str.replaceAll(reg, "$1");
        System.out.println(str);
    }
}

2、用非捕获组进行查找判断和提取

java
package com.atguigu.regular;

import org.junit.jupiter.api.Test;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class TestNonGroup {
    @Test
    public void test1(){
        //找出字母前面的2位数字
        //答案:32,38
        String str = "12332aa438aaf";
        String reg = "\\d{2}(?=[a-z]+)";//肯定式向前查找
        Pattern pattern = Pattern.compile(reg);
        Matcher matcher = pattern.matcher(str);
        while(matcher.find()){
            System.out.println(matcher.group());
        }
    }

    @Test
    public void test2(){
        //找出字母后面的2位数字
        //答案:43
        String str = "12332aa438aaf";
        String reg = "(?<=[a-z])\\d{2}";//肯定式向后查找
        Pattern pattern = Pattern.compile(reg);
        Matcher matcher = pattern.matcher(str);
        while(matcher.find()){
            System.out.println(matcher.group());
        }
    }

    @Test
    public void test3(){
        //找出不在字母前面的2位数字,即陆续找到2位数字,后面不跟字母
        //答案:12,33,43
        String str = "12332aa438aaf";
        String reg = "\\d{2}(?![a-z]+)";//否定式向前查找
        Pattern pattern = Pattern.compile(reg);
        Matcher matcher = pattern.matcher(str);
        while(matcher.find()){
            System.out.println(matcher.group());
        }

    }

    @Test
    public void test4(){
        //找出不在字母后面的2位数字
        //答案:12,33,38
        String str = "12332aa438aaf";
        String reg = "(?<![a-z])\\d{2}";//否定式向后查找
        Pattern pattern = Pattern.compile(reg);
        Matcher matcher = pattern.matcher(str);
        while(matcher.find()){
            System.out.println(matcher.group());
        }
    }
}