一、复习
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 个关键字:
//普通try-catch
try{
可能发生异常的代码
}catch(异常的类型1 参数名e){
异常处理的代码
或
打印或记录异常信息的代码
}catch(异常的类型2 参数名e){
异常处理的代码
或
打印或记录异常信息的代码
}finally{
必须执行的代码。一般是资源释放代码。
}
try(需要自动关闭的资源对象的声明){
可能发生异常的业务代码
}catch(异常的类型2 参数名e){
异常处理的代码
或
打印或记录异常信息的代码
}finally{
如果真有除了资源关闭之外的其他必须执行的代码,可以写这里。
}
throw | throws | |
---|---|---|
作用 | 主动抛出一个异常对象 | 在方法头的签名中,声明当前方法可能发生 xx 异常,且当前方法没处理,交给调用者处理 |
语法形式 | throw 异常对象; | 【修饰符】 返回值类型 方法名(【形参列表】)throws 异常类型列表{} |
二、日期时间
Java 中一共引入了三代的日期时间 API。
2.1 第 1 代
1、java.util.Date
Date 类中大部分方法都已过时。它的对象可以表示一个日期时间的瞬时。
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 对象。
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
。
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
对象表示了特定的地理、政治和文化地区。
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
类是一个抽象类,它为特定瞬间与一组诸如 YEAR
、MONTH
、DAY_OF_MONTH
、HOUR
等 [
日历字段](../../java/util/Calendar.html#fields)
之间的转换提供了一些方法,并为操作日历字段(例如获得下星期的日期)提供了一些方法。Calendar
提供了一个类方法 getInstance
,以获得此类型的一个通用的对象。
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
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
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
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:时间间隔
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:是一个枚举类,有几种预定义的风格
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 代的转换
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 代
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 代有一些问题:
- 麻烦
- 线程安全问题(等讲完线程安全部分再回来看)
- 对象的可变性问题
- 闰秒问题
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)范围的字符
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()
方法的结果
建议不是特别的需求,使用字符串就用"",可以共享对象,从而节约内存。
内存区域 | JDK7 | JDK8 |
---|---|---|
方法区 | - 实现为永久代 - 类信息(类元数据) - 运行时常量池 - JIT 编译后的代码 - 静态变量 | -实现为元空间,位于本地内存(Native Memory) - 类信息(类元数据) - 运行时常量池 - JIT 编译后的代码 |
堆 | - 对象实例(包括数组) - 字符串常量池(JDK7 从永久代移至堆) | - 对象实例(包括数组) - 字符串常量池 - 静态变量(JDK8 从永久代移至堆) |
Java 虚拟机栈 | - 栈帧:局部变量表、操作数栈、动态链接、方法出口 | 与 JDK7 相同 |
本地方法栈 | - Native 方法调用的栈帧 | 与 JDK7 相同 |
程序计数器 | 当前线程执行的字节码指令地址 | 与 JDK7 相同 |
4、字符串对象个数
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
}
}
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 个字符占几个字节?
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[] ,【编码方式】)
:解密的过程
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]
:a
、b
或 c
(简单类)
[^abc]
:任何字符,除了 a
、b
或 c
(否定)
[a-zA-Z]
:a
到 z
或 A
到 Z
,两头的字母包括在内(范围)
(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 运算符
XY:X 后跟 Y
X|
Y:X 或 Y
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(正则)
:按照正则对字符串进行拆分
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);
}
}
}
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、捕获组的提取与反向引用
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、用非捕获组进行查找判断和提取
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());
}
}
}