第5章〓Java API 本章介绍一些常用的Java API,包括字符串、时间与日期、数学与随机数、正则表达式和容器等接口和类。 本章要点 字符串; 时间与日期; 数学与随机数; 系统相关; 正则表达式; 容器。 5.1字符串 java.lang包提供了字符串相关的一些类,如String、StringBuffer、StringBuilder等类。字符串是应用非常频繁的对象,本节主要介绍这些字符串类的使用方法。 5.1.1String类 String是一个最终类(final类型),它表示一个字符串常量。Java字符串常量使用双引号包括的一串字符,字符串由0个或任意多个字符组成,例如: "Java Programming." Java编译器自动为每一个字符串常量生成一个String类的实例,因此可以用字符串常量直接初始化一个String对象,例如: String s="Java Programming."; 字符串常量,一旦创建之后其值就不能再改变。但Java提供了对字符串连接符号(+)用于连接其他字符串,例如: String s = "Hello, " + name; 假设变量name的值是“Liming”,那么字符串变量s的值为“Hello, Liming”。 声明字符串除了上述方式,也可以使用String类的构造方法创建字符串对象,String类中提供了多个构造方法,常见的构造方法见表5.1。 表5.1String类常见的构造方法 方法名说明 String()创建一个空字符串 String(byte[] bytes)将一个byte数组构造为字符串 String(String s)使用字符串构造为String对象 String(char[] chars)将一个字符数组构造为字符串 String类提供了很多操作字符串的方法,包括字符串的比较、查找、分隔等操作,表5.2列出String类的主要方法。 表5.2String类的主要方法 方法名说明 byte[] getBytes()返回字符串的byte数组形式 char charAt(int index)返回指定索引处的字符 int compareTo(String s)按照字母表的顺序比较两个字符串的大小关系,如相等返回0,否则返回两个字符串对应字符的差值 boolean regionMatches(boolean ignoreCase, int toffset, String other, int offset, int len)测试两个字符串的区域是否相等,即模式匹配,匹配成功则返回true,否则返回false boolean startsWith(String prefix, int toffset)判断字符串是否以指定前缀开始 boolean endsWith(String suffix)判断该字符串是否以suffix后缀结束 int lastIndexOf(int ch)返回ch在该字符串中最后一次出现时的索引值 String substring(int beginIndex, int endIndex)取得该字符串在[beginIndex,endIndex)范围内的子字符串 String concat(String str)将str连接到该字符串后并返回 String replace(char oldChar, char newChar)该方法有重载,将字符oldChar替换为newChar String[] split(String regex)将字符串根据给定的正则表达式进行拆分 String toLowerCase()返回字符串的小写字母形式 String toUpperCase()返回字符串的大写字母形式 String trim()清除字符串两端的空格 char[] toCharArray()返回字符串的字符数组形式 static String valueOf(type types)该方法有重载,返回types的字符串形式 int length()返回字符串长度 boolean equals(String s)判断两个字符串的内容是否相等 int indexOf(int ch)返回ch在该字符串中首次出现时的索引值 编写一个程序,输入一个身份证号,并实现如下功能: ①验证身份证号的有效性; ②获取其出生年月日; ③判断其籍贯是否为北京市朝阳区。例5.1为String类的实例。 【例5.1】Example5_01.java public class Example5_01 { public static void main(String[] args) { String id = input(); if(valid(id)){ LocalDate birth = getBirth(id); System.out.println("出生日期: " + birth.toString()); //北京朝阳区地区编码110105 String chaoyang = "110105"; System.out.println("他是朝阳群众吗?" + getRegion(id,chaoyang)); } else{ System.out.println("请输入合法身份证号码!"); return; } } //输入身份证号码 public static String input(){ Scanner scanner = new Scanner(System.in); System.out.println("输入身份证号码: "); String id = null; id = scanner.nextLine(); return id; } /** * 根据正则表达式验证身份证有效性 * @param id 身份证号码 * @return */ public static boolean valid(String id){ //身份证正则表达式 String regex = "\\d{17}[\\d|x]|\\d{15}"; if(id.matches(regex)) return true; return false; } /** * 根据身份证号码得到出生日期 * @param id 身份证号码 * @returnLocalDate类型的日期 */ public static LocalDate getBirth(String id){ int year = Integer.parseInt(id.substring(6,10)); int month = Integer.parseInt(id.substring(10,12)); int day = Integer.parseInt(id.substring(12,14)); LocalDate birthdate = LocalDate.of(year,month,day); return birthdate; } /** * 判断身份证号码是否以prefix打头 * @param id 身份证号码 * @param prefix 前缀 * @return */ public static boolean getRegion(String id,String prefix){ if(id.startsWith(prefix)) return true; return false; } } 运行结果: 输入身份证号码: 110105200808081256 出生日期: 2008-08-08 他是朝阳群众吗?true 5.1.2StringBuffer类 尽管String类提供了丰富的方法,但String代表的是字符串常量,所以无法对字符串进行插入、删除等操作。StringBuffer类可用于操作字符串变量,特别是追加、插入等操作,另外,StringBuffer也支持多线程,对于需要对字符串执行同步的程序来讲,使用StringBuffer是较好的选择。 创建StringBuffer对象时可为该对象提供一个字符串缓冲区,默认情况下其容量为16个字符,当然也可以指定缓冲区容量的大小。表5.3列出了StringBuffer类的构造方法。 表5.3StringBuffer类的构造方法 方法名说明 StringBuffer()构造一个空的StringBuffer对象,其缓冲区容量为16个字符 StringBuffer(int capacity)构造一个缓冲区容量为capacity的StringBuffer对象 StringBuffer(String str)构造一个初始内容为str的StringBuffer对象,容量为 16 加上str的长度 StringBuffer类也提供了很多关于字符串操作的方法,有些方法与String类高度雷同,此处不再赘述。表5.4列出了StringBuffer类的追加、插入、删除、反转等常用方法。 表5.4StringBuffer类的常用方法 方法名说明 StringBuffer append(type t)将t追加到此字符串的尾部 StringBuffer insert(int offset,type t)将t插入到此字符串索引为offset的位置 StringBuffer reverse()返回此字符串的反转形式 StringBuffer delete(int start,int end)删除此字符串指定区间[start,end)内的子字符串 StringBuffer deleteCharAt(int index)删除此字符串指定索引处的字符 下面的案例遍历一个数组,并将其元素拼接为一个字符串形式,本程序分别使用String类与StringBuffer类两种方式实现,可通过结果对比二者的效率。 【例5.2】Example5_02.java public class Example5_02 { public static void main(String[] args) { int len = 10000; int[] arr = new int[len]; for (int i=0;i<arr.length;i++){ arr[i] = 2 * i +1; } String s = arrayToStringBuffer(arr); String s1 = arrayToString(arr); } //使用StringBuffer方式拼接字符串 public static String arrayToStringBuffer(int[] arr){ StringBuffer sb = new StringBuffer(); long begin = System.currentTimeMillis(); sb.append("["); for (int x = 0; x < arr.length; x++) { //最后一个元素 if (x == arr.length - 1) { sb.append(arr[x]+"]"); } else { //拼接后为StringBuffer类型 sb.append(arr[x]).append(", "); } } long end = System.currentTimeMillis(); System.out.println("StringBuffer方式消耗时间: " + (end-begin) + "ms"); //StringBuffer类下的toString()方法,返回字符串String类型 return sb.toString(); } //使用String方式拼接字符串 public static String arrayToString(int[] arr){ String sb = ""; long begin = System.currentTimeMillis(); sb = sb + "["; for (int x = 0; x < arr.length; x++) { //最后一个元素 if (x == arr.length - 1) { sb = sb + arr[x]+"]"; } else { //拼接后为StringBuffer类型 sb = sb + arr[x] + ", "; } } long end = System.currentTimeMillis(); System.out.println("String方式消耗时间: " + (end-begin) + "ms"); //StringBuffer类下的toString()方法,返回字符串String类型 return sb.toString(); } } 运行结果: StringBuffer方式消耗时间: 37ms String方式消耗时间: 232ms 本例分别使用StringBuffer类和String类将一个长度为10000的整型数组遍历后拼接为字符串形式。通过运行结果可以看到,StringBuffer类的append()方法追加字符串明显比String类使用“+”拼接的方式效率高。 5.1.3StringBuilder类 StringBuffer是一个线程安全类,提供了对字符串操作的同步控制。同时,Java也提供了另外一个字符串的可变对象,用来高效拼接字符串,它就是StringBuilder类。StringBuilder类是线程不安全的,但支持链式操作,效率比StringBuffer更高。StringBuilder类提供的方法与StringBuffer类似,本节不再说明。仍以例5.2的功能,增加一个方法使用StringBuilder类实现。 /** * 使用StringBuilder类实现字符串拼接 * @param arr * @return */ public static String arrayToStringBuilder(int[] arr){ StringBuilder sb = new StringBuilder(); long begin = System.currentTimeMillis(); sb.append("["); for (int x = 0; x < arr.length; x++) { //最后一个元素 if (x == arr.length - 1) { sb.append(arr[x]+"]"); } else { //拼接后为StringBuffer类型 sb.append(arr[x]).append(", "); } } long end = System.currentTimeMillis(); System.out.println("StringBuilder方式消耗时间: " + (end-begin) + "ms"); //StringBuilder类下的toString()方法,返回字符串String类型 return sb.toString(); } 运行结果: StringBuffer方式消耗时间: 60ms String方式消耗时间: 213ms StringBuilder方式消耗时间: 4ms 通过对比,StringBuilder类的效率最高,String类的效率最低。说明如果大量对字符串进行拼接、插入和删除等操作时,使用StringBuilder是较好的选择。 注意: String类与StringBuffer类、StringBuilder类的不同之处如下。 String类重写了equals()方法,比较两个字符串内容是否相等。StringBuffer类和StringBuilder类没有重写equals()方法,仍然比较两个对象的地址是否相等; StringBuffer类和StringBuilder类用于字符串追加、插入、删除、反转等操作,且其内容可以改变,且字符串拼接的效率明显比String类高; StringBuffer是StringBuilder的线程安全版本,现在很少使用。 5.1.4StringTokenizer类 java.util.StringTokenizer类用于分隔字符串,可以指定分隔符,并提供了遍历字符串的方法。StringTokenizer类的 构造方法与其他 方法如表5.5所示。 表5.5StringTokenizer类的构造方法和其他方法 方法名说明 StringTokenizer(String str)构造一个用来解析str的StringTokenizer对象。Java默认的分隔符是“空格”“制表符('\t')”“换行符('\n')”“回车符('\r')” StringTokenizer(String str,String delim)构造一个用来解析str的StringTokenizer对象,并提供一个指定的分隔符 StringTokenizer(String str, String delim, boolean retrunDelims)构造一个用来解析str的StringTokenizer对象,并提供一个指定的分隔符,同时指定是否返回分隔符 int countTokens()返回nextToken方法被调用的次数 boolean hasMoreTokens()返回是否还有分隔符 boolean hasMoreElements()返回是否还有元素 String nextToken()返回从当前位置到下一个分隔符的字符串 String nextToken(String delim)以指定的分隔符返回结果 【例5.3】Example5_03.java import java.util.StringTokenizer; public class Example5_03 { public static void main(String[] args) { String s = new String("Object-oriented programming (OOP) is a programming" +"paradigm."); StringTokenizer st = new StringTokenizer(s); System.out.println("Token Total: " + st.countTokens()); while(st.hasMoreElements()) { System.out.println(st.nextToken()); } } } 运行结果: Token Total: 7 Object-oriented programming (OOP) is a programming paradigm. 本例构造了无参StringTokenizer对象,使用空格作为分隔符分隔字符串s,通过使用hasMoreElements()和nextToken()方法遍历分隔后的字符串。hasMoreElements()和hasMoreToken()方法是等价的,本例中二者可以互换。 5.2时间与日期 时间和日期是应用程序中经常用到的对象,Java提供了丰富的时间与日期类,包括日期、时间、日历等类。 5.2.1java.util.Date类 java.util.Date类封装了当前的日期和时间,通过Date类可以构建当前的系统时间,该类同时提供设置时间、获取时间、比较时间关系的方法,具体见表5.6。 表5.6java.util.Date类的构造方法和常用方法 方法名说明 Date()使用当前日期和时间来初始化对象 Date(long ms)参数是从1970年1月1日起的毫秒数,构造Date对象 void setTime(long ms)用自1970年1月1日00:00:00 GMT以后time毫秒数设置时间和日期 long getTime()返回自1970年1月1日 00:00:00 GMT以来此Date对象表示的毫秒数 boolean after(Date date)若调用此方法的Date对象在指定日期之后返回true,否则返回false boolean before(Date date) 若调用此方法的Date对象在指定日期之前返回true,否则返回false int compareTo(Date date)比较调用此方法的Date对象和指定日期。两者相等时返回0。调用对象在指定日期之前则返回负数。调用对象在指定日期之后则返回正数 5.2.2java.sql.Date类 java.sql.Date为java.util.Date的子类,该类是一个封装了毫秒值并支持JDBC操作的日期类,该类是为了与SQL DATE类型保持一致而特设的一个日期类,规范化的java.sql.Date只包含年月日信息,时分秒等都置零。该类的 构造方法和常用 方法见表5.7。 表5.7java.sql.Date类的构造方法和常用方法 方法名说明 Date(long ms)使用给定的毫秒数构造Date对象 void setTime(long ms)用给定的毫秒数设置日期 static Date valueOf(String s)将JDBC日期转义格式的字符串转换为日期类型 5.2.3Calendar类 java.util.Calendar类是一个抽象类,表示日历对象,它提供了丰富的常量,如YEAR、MONTH、DATE_OF_MONTH等表示 年、月、日等,同时提供了操作日历字段的一些方法,如get()、set()等方法。表5.8列出了Calendar类的 常量和常用方法。 表5.8java.util.Calendar类的常量和常用方法 常量和常用方法说明 Calendar.YEAR年份 Calendar.MONTH月份,注意月份范围为0~11,0代表1月,以此类推 Calendar.DATE日期 Calendar.DAY_OF_MONTH日期 Calendar.HOUR12小时制的小时 Calendar.HOUR_OF_DAY24小时制的小时 Calendar.MINUTE分钟 Calendar.SECOND秒 Calendar.DAY_OF_WEEK星期几 getInstance()返回一个日历对象实例 final void setTime(Date date)用给定的日期设置日历 final java.util.Date getTime()返回一个java.util.Date日期类型 void set(int year,int month,int day)根据年、月、日设置日历 int get(int field)根据日历常量值获取当前日历信息 【例5.4】Example5_04.java import java.time.LocalDate; import java.util.Calendar; import java.util.Date; public class Example5_04 { public static void main(String[] args) { java.util.Date date = new java.util.Date(); System.out.println("Date原始输出: " + date.toString()); System.out.println("距1970-1-1 0时的毫秒数: " + date.getTime()); //通过java.util.Date构造java.sql.Date对象 java.sql.Date dbDate = new java.sql.Date(date.getTime()); //可将java.sql.Date对象转换为LocalDate对象 LocalDate localDate = dbDate.toLocalDate(); System.out.println("LocalDate:" + localDate); //Calendar类是抽象类 Calendar calendar = Calendar.getInstance(); //可通过如下两种方式为日历对象设置时间 calendar.set(2021,11,06); calendar.setTime(date); //格式化日期 yyyy年M月d日 hh:mm:ss String sDate = calendar.get(Calendar.YEAR) + "年" + (calendar.get(Calendar.MONTH) + 1) + "月"+ calendar.get(Calendar.DAY_OF_MONTH) + "日" + calendar.get(Calendar.HOUR_OF_DAY) + ":"+ calendar.get(Calendar.MINUTE) + ":" + calendar.get(Calendar.SECOND); System.out.println(sDate); } } 运行结果: Date原始输出: Fri Nov 05 23:23:32 CST 2021 距1970-1-1 0时的毫秒数: 1636125812156 LocalDate:2021-11-05 2021年11月5日23:23:32 5.2.4LocalDate类 java.time包提供了日期和时间的API,包括的类型如下。 本地日期和时间: LocalDate、LocalTime、LocalDateTime; 带时区的日期和时间: ZonedDateTime; 时刻: Instant; 时区: ZoneId、ZoneOffset; 时间间隔: Duration。 java.time.LocalDate是Java 8新增的一个日期类,该类提供了对日期(年月日)的简便操作。该类不能代表时间线上的即时信息,只是日期的描述。在LocalDate类中提供了两个获取日期对象的方法: now()和of(int year, int month, int dayOfMonth),可通过这两个方法构造一个LocalDate对象,例如: //构造日期为2021-11-11 LocalDate date = LocalDate.of(2021,11,11); //以默认时区的系统时间构造LocalDate对象 LocalDate now = LocalDate.now(); 表5.9列出了LocalDate类的主要方法。 表5.9java.time.LocalDate类的主要方法 方法名说明 static LocalDate now()返回当前日期 static LocalDate of(int year,int m,int d)根据参数指定的年月日设置日期 static LocalDate parse(CharSequence text)将特定格式的字符串转换为LocalDate类型 int getDayOfMonth()获取当前日期是所在月的第几天 int getMonthValue()获取当前日期所在月份数值 boolean isLeapYear()判断当前日期是否为闰年 LocalDate with(TemporalField t, long v)给特定时间字段赋予新值 LocalDate withDayOfMonth(int day)将当前日期的日替换为参数值 LocalDate withDayOfYear(int day)将当前日期的天数替换为参数值 LocalDate withMonth(int month)将当前日期的月份替换为参数值 LocalDate withYear(int year)将当前日期的年份替换为参数值 LocalDate minusDays(long days)将当前日期减少参数天 LocalDate minusWeeks(long weeks)将当前日期减少参数周 LocalDate minusMonths(long months)将当前日期减少参数月 LocalDate minusYears(Long years)将当前日期减少参数年 LocalDate plusDays(long days)将当前日期增加参数天 LocalDate plusWeeks(long weeks)将当前日期增加参数周 LocalDate plusMonths(long months)将当前日期增加参数月 LocalDate plusYear(long years)将当前日期增加参数年 注意: Java 8新修订的日期和时间类较旧版本做了如下方面的完善。 月份Month的范围用1~12表示1~12月,星期Week的范围用1~7表示 星期一至星期日; 严格区分了时刻、本地日期、本地时间和带时区的日期和时间,对日期和时间的运算更加简便; 新API的类型几乎都是不变类型(类似于String),不必担心值被修改。 5.2.5LocalTime类 java.time.LocalTime类是一个时间类,提供了包括时、分、秒和纳秒等时钟信息,与LocalDate类一样,该类不能代表时间线上的即时信息,只是时间的描述。在LocalTime类中提供了获取时间对象的方法,与LocalDate用法类似。同时LocalTime类也提供了与日期类相对应的时间格式化、增减时分秒等常用方法,这些方法与日期类相对应。 //构造时间为10:10:35 LocalTime localTime = LocalTime.of(10,10,35); //以默认时区的系统时钟构造LocalTime对象 LocalTime time = LocalDTime.now(); 表5.10列出了LocalTime类的主要方法。 表5.10java.time.LocalTime类的主要方法 方法名说明 static LocalTime now()返回当前时间 static LocalTime of(int hour,int m,int s)根据参数指定的时分秒设置时间 static LocalTime parse(CharSequence text)将特定格式的字符串转换为LocalTime类型 int getNano()获取当前时间的纳秒数 int getHour()获取当前时间的小时 int getMinute()获取当前时间的分钟 int getSecond()获取当前时间的秒 LocalDateTime atDate(LocalDate date)给特定日期字段赋予新值 LocalTime with(TemporalField t, long v)给特定时间字段赋予新值 LocalTime withHour(int hour)将当前时间的小时替换为参数值 LocalTime withMinute(int minute)将当前时间的分钟替换为参数值 LocalTime withSecond(int second)将当前时间的秒替换为参数值 LocalTime withNano(int nano)将当前时间的纳秒替换为参数值 LocalTime minusHours(long hours)将当前时间减少参数小时 LocalTime minusMinutes(long min)将当前时间减少参数分钟 LocalTime minusSeconds(long sec)将当前时间减少参数秒 LocalTime minusNanos(long nanos)将当前时间减少参数纳秒 LocalTime plusHours (long hours)将当前时间增加参数小时 LocalTime plus Minutes(long min)将当前时间增加参数分钟 LocalTime plusSeconds(long sec)将当前时间增加参数秒 LocalTime plusNanos(long nanos)将当前时间增加参数纳秒 boolean isBefore(LocalTime time)当前时间对象与参数比较,若在参数之前返回true boolean isAfter(LocalTime time)当前时间对象与参数比较,若在参数之后返回true 5.2.6LocalDateTime类 java.time.LocalDateTime类表示一个本地日期和时间,相当于LocalDate和LocalTime二者的综合体。 【例5.5】Example5_05.java import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; public class Example5_05 { public static void main(String[] args) { LocalDateTime localDateTime1 = LocalDateTime.now(); LocalDateTime localDateTime2 = LocalDateTime.of(2021, 11, 11, 18, 56, 52); System.out.println(localDateTime1); System.out.println(localDateTime2); System.out.println("localDateTime1 星期: " + localDateTime1.getDayOfWeek()); //将LocalDateTime转换为LocalDate与LocalTime LocalDate date = localDateTime1.toLocalDate(); LocalTime time = localDateTime1.toLocalTime(); System.out.println(date); System.out.println(time); int hour = time.getHour(); int minute = time.getMinute(); int second = time.getSecond(); int nano = time.getNano(); System.out.println("时 -> " + hour); System.out.println("分 -> " + minute); System.out.println("秒 -> " + second); System.out.println("Nano -> " + nano); } } 运行结果: 2021-11-06T00:37:23.557737600 2021-11-11T18:56:52 localDateTime1 星期: SATURDAY 2021-11-06 00:37:23.557737600 时 -> 0 分 -> 37 秒 -> 23 Nano -> 557737600 注意: LocalDateTime、LocalDate和LocalTime都严格按照国际标准ISO 8601规定的日期和时间格式进行打印,ISO 8601规定日期和时间之间使用分隔符T隔开,标准格式如下。 日期: yyyyMMdd; 时间: HH:mm:ss; 带毫秒的时间: HH:mm:ss.SSS; 日期和时间: yyyyMMdd'T'HH:mm:ss; 带毫秒的日期和时间: yyyyMMdd'T'HH:mm:ss.SSS。 LocalDateTime、LocalDate和LocalTime三个类都提供了parse()方法,可将符合ISO 8601格式的字符串转换为相应的日期和时间类型,例如: //将字符串转换为LocalDateTime类型 LocalDateTime dt = LocalDateTime.parse("2021-11-06T08:16:32"); //将字符串转换为LocalDate类型 LocalDate d = LocalDate.parse("2021-11-06"); //将字符串转换为LocalTime类型 LocalTime t = LocalTime.parse("08:16:32"); 5.2.7Instant类 java.time.Instant 类代表某个时间。其内部由两个long字段组成,第一部分保存的是标准Java计算时代(就是1970年1月1日开始)到现在的秒数,第二部分保存的是纳秒数。表5.10列出了Instant类的主要方法。 表5.11java.time.Instant类的主要方法 方法名说明 now()从系统时钟获取当前瞬时 now(Clock clock)从指定时钟获取当前瞬时 ofEpochSecond(long epochSecond)使用从标准Java计算时代开始的秒数获得一个Instant的实例 ofEpochMilli(long epochMilli)使用从标准Java计算时代开始的秒数获得一个Instant的实例 getEpochSecond()从19700101T00:00:00Z的Java时代获取秒数 getNano()获取Instant对象表示的纳秒数 parse(CharSequence text)从一个指定格式的文本字符串获取一个Instant的实例 from(TemporalAccessor tenporal)从时间对象获取一个Instant的实例 【例5.6】Example5_06.java import java.time.Instant; public class Example5_06 { public static void main(String[] args) { //创建Instant对象 Instant instant = Instant.now(); //以ISO 8601格式输出 System.out.println(instant); //java.util.Date-->Instant类型 instant = Instant.ofEpochMilli(new java.util.Date().getTime()); //将字符串转换为Instant类型 instant = Instant.parse("2021-11-11T10:10:35Z"); System.out.println(instant); Instant in = instant.plusSeconds(30); System.out.println(in); } } 运行结果: 2021-11-05T17:19:56.009729500Z 2021-11-11T10:10:35Z 2021-11-11T10:11:05Z 5.2.8Duration类和Period类 Duration类基于时间值,其作用范围是天、时、分、秒、毫秒和纳秒,而Period类基于日期类,计算两个日期之间的差值。 【例5.7】Example5_07.java import java.time.Duration; import java.time.LocalDateTime; import java.time.Period; public class Example5_07 { public static void main(String[] args) { LocalDateTime now = LocalDateTime.now(); LocalDateTime ago = LocalDateTime.of(1921,07,01,0,0,0); //创建Duration对象 Duration duration = Duration.between(ago,now); System.out.println("间隔天: " + duration.toDays()); System.out.println("间隔小时: " + duration.toHours()); System.out.println("间隔分钟: " + duration.toMinutes()); //创建Period对象 Period period = Period.between(ago.toLocalDate(),now.toLocalDate()); System.out.println("间隔年: " + period.getYears()); System.out.println("间隔月: " + period.getMonths()); System.out.println("间隔天: " + period.getDays()); } } 运行结果: 间隔天: 36653 间隔小时: 879673 间隔分钟: 52780394 间隔年: 100 间隔月: 4 间隔天: 5 5.2.9日期格式化 1. DateFormat类 java.text.DateFormat类提供了两个功能: 定义日期时间格式,实现String与日期时间之间的转换。DateFormat是一个抽象类,SimpleDateFormat是其实现类。该类的基本用法如下。 //创建java.util.Date对象 Date date = new Date(); //实例化DateFormat对象,并指定日期格式为"yyyy-MM-dd HH:mm:ss" DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //将Date类型转换为上述格式的字符串 String s = dateFormat.format(date); //将"2021-11-06 13:10:35"转换为java.util.Date类型 String d = "2021-11-06 13:10:35"; date = dateFormat.parse(d); 可以看到,SimpleDateFormat类提供的构造方法用来指定日期和时间格式,format()方法可以将一个Date对象转换为相应格式的字符串; 反之,parse()方法可以将指定格式的字符串转换为Date类型。SimpleDateFormat类的日期和时间格式由模式字符串指定,该类定义了模式字母,所有其他字符'A'~'Z'和'a'~'z'都被保留,具体见表5.12。 表5.12模式字母表 字母日期或时间元素类型示例 GEra标志符TextAD y年Year2021; 21 M年中的月份MonthJuly; Jul; 07 w年中的周数Number36 W月份中的周数Number2 D年中的天数Number310 d月份中的天数Number7 E星期中的天数TextSunday; Sun aAM/PM标志TextPM H一天中的小时数(0~23)Number0 k一天中的小时数(1~24)Number24 K一天中的小时数(0~11)Number0 h一天中的小时数(1~12)Number12 m小时中的分钟数Number30 s分钟中的秒数Number59 S毫秒数Number988 2. DateTimeFormatter类 java.text.DateFormat类主要实现在java.util.Date进行格式化显示。如果要对LocalDateTime进行格式化显示,需要使用DateTimeFormatter类。DateTimeFormatter是不变对象,并且是线程安全的。 【例5.8】Example5_08.java import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.time.temporal.TemporalAccessor; import java.util.Date; public class Example5_08 { public static void main(String[] args) { //创建java.util.Date对象 Date date = new Date(); //实例化DateFormat对象,并指定日期格式为"yyyy-MM-dd HH:mm:ss" DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //将Date类型转换为上述格式的字符串 String s = dateFormat.format(date); //将"2021-11-06 13:10:35"转换为java.util.Date类型 String d = "2021-11-06 10:10:35"; try { date = dateFormat.parse(d); } catch (ParseException e) { e.printStackTrace(); } //创建LocalDateTime对象 LocalDateTime dt = LocalDateTime.now(); //定义日期格式 var f1 = DateTimeFormatter.ofPattern("yyyy年MM月dd日"); String s1 = dt.format(f1); System.out.println(s1); //定义时间格式 DateTimeFormatter f2 = DateTimeFormatter.ofPattern("HH:mm:ss"); String s2 = dt.format(f2); System.out.println(s2); TemporalAccessor ta = f1.parse("2021年09月10日"); System.out.println(ta); } } 运行结果: 2021年11月07日 14:10:22 {},ISO resolved to 2021-09-10 5.3数值与随机数 本节主要介绍有关初等数学的相关函数操作类、随机类和包装类。包装类提供了对Java的八种基本数据类型封装的方法。 5.3.1Math类 java.lang.Math类提供了大量类方法,用于求解基本数学运算,如初等指数、对数、三角函数、平方根、绝对值、近似值等。Math类的常见方法见表5.13。 表5.13java.lang.Math类的常见方法 方法名说明 abs(a)计算绝对值 sqrt(a)计算平方根 ceil(a,b)计算大于参数的最小整数,简称天花板数 floor(a)计算小于参数的最小整数,简称地板数 round(a)计算小数进行四舍五入后的结果 max(a,b)计算两个数的较大值 min(a,b)计算两个数的较小值 random()生成一个大于0.0且小于1.0的随机值 pow(a,b)计算ab log(a)计算以自然数为底数的对数值 sin(a)求参数的正弦值 cos(a)求参数的余弦值 tan(a)求参数的正切值 asin(a)求参数的反正弦值 acos(a)求参数的反余弦值 atan(a)求参数的反正切值 toDegrees(a)将参数转换为角度 toRadians(a)将角度转换为弧度 5.3.2Random类 java.util.Random类可以生成指定范围的随机数,包括整数和浮点数。Random类中提供了两个构造方法和随机生成整数和浮点数的方法。Random类的 构造方法和 常见方法见表5.14。 表5.14java.util.Random类的构造方法和常见方法 方法名说明 Random()构造一个随机数生成器 Random(long seed)用种子seed构造一个随机数生成器 boolean nextBoolean()生成一个boolean类型的随机数 float nextFloat()生成一个float类型的随机数 double nextDouble()生成一个double类型的随机数 int nextInt()生成一个int类型的随机数 int nextInt(int bound)生成一个[0,bound]范围内int类型的随机数 int nextLong()生成一个long类型的随机数 Random类的两个构造方法,无参构造方法具有更强的随机性,通过它创建的Random实例对象每次使用的种子都是随机的,因此每个对象产生的随机数不同。如果希望创建的多个Random实例对象产生相同的随机数,则使用带有种子值的构造方法,传入相同的种子值即可,如例5.9所示。 【例5.9】Example5_09.java import java.util.Random; public class Example5_09 { public static void main(String[] args) { testRandom(); } public static void testRandom() { System.out.println("Random不设置种子: "); for (int i = 0; i < 5; i++) { Random random = new Random(); for (int j = 0; j < 10; j++) { System.out.print(" " + random.nextInt(100) + "\t"); } System.out.println(""); } System.out.println(""); System.out.println("Random设置种子: "); for (int i = 0; i < 5; i++) { Random random = new Random(); random.setSeed(100); for (int j = 0; j < 10; j++) { System.out.print(" " + random.nextInt(100) + "\t"); } System.out.println(""); } } } 某次运行结果如下。 Random不设置种子: 3121674476991779789 58178063447765739867 12353692708342764968 897573571767059257 94283740655343145312 Random设置种子: 15507488916636882313 15507488916636882313 15507488916636882313 15507488916636882313 15507488916636882313 例5.10中用StringBuilder和Random类提供了两种生成验证码的方法,其一是生成一个4位数字组成的验证码,其二是生成一个由数字和字母组成的4位验证码。 【例5.10】Example5_10.java import java.util.Random; public class Example5_10 { public static void main(String[] args) { System.out.println("四位数字: " + numberCode()); System.out.println("四位字符: " + stringCode()); } //生成4位数字组成的验证码 static StringnumberCode(){ Random r1 = new Random(); int i = 0 ; //随机数不能小于1000 i = r1.nextInt(10000); while(true){ if(i<1000) i = r1.nextInt(10000); else break; } return String.valueOf(i); } //生成4位字符组成的验证码 static String stringCode(){ Random r2 = new Random(); //构造随机数产生的范围 String s = "abcdefghigklmnopqrstuvwxyz0123456789"; StringBuilder s1 = new StringBuilder(s); StringBuilder code = new StringBuilder(""); for(int i=0;i<4;i++){ int index = r2.nextInt(36); code.append(s1.charAt(index)); } return code.toString(); } } 某次运行结果如下。 四位数字: 7169 四位字符: i39s 5.3.3包装类 Java作为一种面向对象的语言,类将方法和数据有机整合为一体。但Java提供的8种基本数据类型并不符合面向对象的思想,特别是在一些场景下,需要把基本数据类型作为对象来使用。为了解决这样的问题,JDK提供了包装类,将基本数据类型封装为引用数据类型。表5.15列出了基本数据类型与包装类的对应关系。 表5.15基本数据类型对应的包装类 基本数据类型包装类基本数据类型包装类 byteBytefloatFloat booleanBooleanintInteger charCharacterlongLong doubleDoubleshortShort 包装类提供了丰富的方法和常量,本节以Integer类为例说明包装类的基本用法,表5.16列出了 Integer类的主要方法。 表5.16Integer类的主要方法 方法名说明 xxxxxxValue()如floatValue(),返回指定类型的数值 static int parseInt(String s)将由数字组成的字符串转换为int类型 static Integer valueOf(String s)将字符串s转换为Integer类型 static String toBinaryString(int i)将参数i转换为二进制形式的字符串 static String boHexString(int i)将参数i转换为十六进制形式的字符串 static String toOctalString(int i)将参数i转换为八进制形式的字符串 【例5.11】Example5_11.java public class Example5_11 { public static void main(String[] args) { Integer i = 100; //可以直接将Integer类型的变量赋给int类型变量 int j = i; double d = i.doubleValue(); String s = "1024"; //将String类型转换为int类型 j = Integer.parseInt(s); //使用valueOf()方法将字符串s转换为Integer类型 i = Integer.valueOf(s); //将j转换为十六进制的字符串 System.out.println(Integer.toHexString(j)); } } 运行结果: 400 注意: 基本数据类型与对应包装类的不同之处如下。 包装类型可以是null,而基本数据类型不可以; 包装类可用于泛型,而基本数据类型不可以; 基本数据类型与包装类占用的内存空间不同,基本数据类型较包装类更高效。 从Java 9之后,Integer不再推荐使用其构造方法创建Integer对象,这意味着int类型与Integer类型不再使用装箱、拆箱的方法进行转换,效率更高,对于其他包装类也是如此。 5.3.4BigInteger类与BigDecimal类 1. BigInteger类 应用开发中难免遇到一些超大的数,超出基本数据类型的取值范围,那么Java提供了两个类: BigInteger和BigDecimal,分别表示超大的整数和浮点数。java.math.BigInteger用于表示任意大小的整数,其内部使用一个int[]数组来模拟一个大整数。java.math.BigDecimal表示一个任意大小且精度完全准确的浮点数。BigInteger类的 构造方法和其他 方法见表5.17。 表5.17BigInteger类的构造方法和其他方法 方法名说明 BigInteger(String val)将字符串val构造为BigInteger对象 BigInteger abs()返回大整数的绝对值 BigInteger add(BigInteger val)返回两个大整数的和 BigInteger andNot(BigInteger val)返回两个大整数的按位与非结果 BigInteger and(BigInteger val)返回两个大整数的按位与的结果 BigInteger divide(BigInteger val)返回两个大整数的商 BigInteger max(BigInteger val)返回两个大整数的较大者 BigInteger min(BigInteger val)返回两个大整数的较小者 BigInteger mod(BigInteger val)用当前大整数对val求模 BigInteger multiply(BigInteger val)返回两个大整数的积 BigInteger negate()返回当前大整数的相反数 BigInteger not()返回当前大整数的非 BigInteger or(BigInteger val)返回两个大整数的按位或 BigInteger pow(BigInteger val)返回当前大整数的val幂次方 BigInteger reminder(BigInteger val)返回当前大整数除以val的余数 BigInteger xor(BigInteger val)返回两个大整数的异或 BigInteger substract(BigInteger val)返回两个大整数相减的结果 int intValue()返回大整数的int类型的值 long longValue()返回大整数的long类型的值 double doubleValue()返回大整数的double类型的值 float floatValue()返回大整数的float类型的值 byte[] toByteArray(BigInteger val)将大整数二进制反码保存在byte类型的数组 String toString()将当前大整数转换成十进制的字符串形式 long longValueExact()返回大整数的准确long类型 通过表5.17可知,大整数的算术和逻辑运算不能再使用传统的计算方式,必须使用BigInteger类提供的方法进行相应计算, 如例5.12所示。 【例5.12】Example5_12.java import java.math.BigInteger; public class Example5_12 { public static void main(String[] args) { BigInteger v1 = new BigInteger("99999"); BigInteger v2 = new BigInteger("99"); //幂运算 BigInteger n = v1.pow(10); System.out.println("原始数据BigInteger: " + n); System.out.println("转换为long类型: " + n.longValue()); try{ //使用longValueExact()转换为long类型 //超出long的范围时会抛出ArithmeticException System.out.println(n.longValueExact()); }catch (ArithmeticException e){ System.out.println(e.getMessage()); } //加法运算 n = v1.add(v2); System.out.println(n); //非运算 n = v2.not(); System.out.println(n); } } 运行结果: 原始数据BigInteger: 99990000449988000209997480020999880000449999000001 转换为long类型: 485031440369308097 BigInteger out of long range 100098 -100 2. BigDecimal类 java.math.BigDecimal类用来对超过16位有效位的浮点数进行精确运算,与BigInteger类一样,我们也不能使用传统的算术运算直接对BigDecimal类型的对象进行计算,而必须调用其提供的相应方法计算,方法中的参数也必须是BigDecimal对象。表5.18列出了BigDecimal类的构造方法和主要方法。 表5.18BigDecimal类的构造方法和主要方法 方法名说明 BigDecimal(String val)创建一个具有参数所指定以字符串表示的数值的对象 BigDecimal(double val)创建一个具有参数所指定双精度值的对象 BigDecimal setScale(int s,RoundingMode mode) 设置小数点保留位数及舍入方式 BigDecimal add(BigDecimal val)返回两个大浮点数的和 BigDecimal substract(BigDecimal val)返回两个大浮点数相减的结果 BigDecimal multiply(BigDecimal val)返回两个大浮点数的积 BigDecimal divide(BigDecimal val)返回两个大浮点数的商 BigDecimal divide(BigDecimal val,int scale, int mode)返回两个大浮点数的商,参数val表示除数,参数scale表示小数点保留位数,参数mode表示舍入模式 例5.13以计算房屋公积金贷款的利息为例,计算本息合计金额。使用NumberFormat类设置货币符号及百分比格式,然后使用BigDicemal类表示大浮点数对象并进行算法运算。 【例5.13】Example5_13.java import java.math.BigDecimal; import java.math.RoundingMode; import java.text.NumberFormat; public class Example5_13 { public static void main(String[] args) { //建立货币格式化引用 NumberFormat currency = NumberFormat.getCurrencyInstance(); //建立百分比格式化引用 NumberFormat percent = NumberFormat.getPercentInstance(); //百分比小数点最多3位 percent.setMaximumFractionDigits(4); //贷款金额 BigDecimal loanAmount = new BigDecimal("395200.99"); //公积金贷款利率 BigDecimal interestRate = new BigDecimal("0.035"); //计算贷款一年的利息 BigDecimal interest = loanAmount.multiply(interestRate); //应还金额,小数点保留两位且四舍五入 BigDecimal total = loanAmount.add(interest).setScale(2,RoundingMode.HALF_UP); System.out.println("贷款金额:\t" + currency.format(loanAmount)); System.out.println("利率:\t" + percent.format(interestRate)); System.out.println("利息:\t" + currency.format(interest)); System.out.println("本息合计: \t" + total); } } 运行结果: 贷款金额:¥395,200.99 利率:3.5% 利息:¥13,832.03 本息合计: 409033.02 注意: BigDecimal类提供了多种构造方法,建议使用参数类型为String的构造方法构造BigDecimal对象。由于浮点数无法使用二进制精确表示,计算机表示浮点数由两部分组成: 指数和尾数,因此会失去一定的精确度,有些浮点数运算也会产生一定的误差。建议进行商业计算,特别是小数点保留指定位数时,使用BigDecimal类。 5.4系统相关类 Java提供了两个系统相关信息的类,System类代表当前Java程序的运行平台,Runtime类表示当前JVM的工作信息。 5.4.1System类 java.lang.System类是应用最为广泛的一个类,该类提供的类变量out在很多案例中都有应用,System类定义了一些与系统相关的属性和方法,具体见表5.19。 表5.19System类的方法 方法名说明 static void exit(int status)该方法用于终止当前正在运行的JVM,参数status表示状态码,若非0表示异常终止 static void gc()运行垃圾回收器回收垃圾 static long currentTimeMillis()以毫秒为单位返回当前时刻距离19700101 0时的时间间隔 static void arraycopy(Object src, int srcpos, Object dest, int destPos, int length)从数组src复制到数组dest,复制从指定位置开始,到目标数组的指定位置结束 static Properties getProperties()获取当前的系统属性 static String getProperty(String key)获取指定名称的系统属性 此外,System类还提供了以下三个类变量。 static PrintStream out: 标准输出流; static PrintStream err: 标准错误输出流; static InputStream in: 标准输入流。 out和err都属于PrintStream类型,而PrintStream类提供了诸如println()、printf()、print()等打印方法。因此, 经常借助System类的类变量实现向控制台输出。 in为InputStream类型,InputStream提供了read()方法,可以通过标准输入(键盘)接收一个字节的数据。 【例5.14】Example5_14.java import java.io.IOException; import java.util.Enumeration; import java.util.Properties; public class Example5_14 { public static void main(String[] args) { char src[] = new char[10]; System.out.println("请输入10个字符: "); //从键盘读入10个字符 for(int i=0;i<src.length;i++){ try{ src[i] = (char)System.in.read(); }catch (IOException e){ e.printStackTrace(); } } for(int i=0;i<src.length;i++) System.out.print(src[i] + " "); System.out.println(); //数组复制 char[] dest = new char[5]; //u将数组src的前5个元素复制到数组dest System.arraycopy(src,0,dest,0,5); String s = new String(dest); System.out.println("复制的数组内容: " + s); //获取系统日期 long time = System.currentTimeMillis(); System.out.println("从1970-01-01 0时到现在的毫秒数:" + time); //获取系统信息 Properties p = System.getProperties(); Enumeration en = p.propertyNames(); while(en.hasMoreElements()){ String key = (String)en.nextElement(); if("java.vm.version".equals(key))//仅输出JVM版本信息 System.out.println(key + ":\t" + System.getProperty(key)); } } } 运行结果: 请输入10个字符: hello,java h e l l o , j a v a 复制的数组内容: hello 从1970-01-01 0时到现在的毫秒数:1636698485533 java.vm.version:16.0.2+7-67 上述例子使用System类从键盘上接收了10个字符存入数组src,然后使用arraycopy()方法将src的前5个字符复制到数组dest。又使用System类获取系统时间和系统信息等。注意本例使用Enumeration接口遍历Properties对象,相关方法在5.6节介绍。 5.4.2Runtime类 Runtime类表示Java虚拟机运行时的状态,它用于封装Java虚拟机的进程。注意,每次java命令启动虚拟机都对应一个Runtime实例,并且Runtime属于单例模式,有且仅有一个Runtime实例。Runtime实例的创建采用如下方式。 Runtime runtime = Runtime.getRuntime(); 表5.20列出了Runtime类的主要方法。 表5.20Runtime类的主要方法 方法名说明 Runtime getRunTime()该方法返回一个Runtime实例 Process exec(String command)throws IOException根据指定路径command返回一个进程对象 long freeMemory()以字节为单位返回当前JVM的空闲内存 long maxMemory()以字节为单位返回JVM将尝试使用的最大内存量 long totalMemory()以字节为单位返回JVM中内存总量 static void gc()运行垃圾回收器回收垃圾 int availableProcessors()返回Java虚拟机可用的处理器数 【例5.15】Example5_15.java public class Example5_15 { public static void main(String[] args) { //创建Runtime实例 Runtime runtime = Runtime.getRuntime(); //获取JVM内存信息 System.out.println("total memory:" + runtime.totalMemory()/(1024*1024) + "MB"); System.out.println("max memory:" + runtime.maxMemory()/(1024*1024) + "MB"); System.out.println("free memory:" + runtime.freeMemory()/(1024*1024) + "MB"); //调用垃圾回收器 runtime.gc(); try { //运行计算器 Process process = runtime.exec("calc.exe"); Thread.sleep(1000 * 5); //5s后关闭计算器进程 process.destroy(); } catch (Exception e) { e.printStackTrace(); } } } 运行结果: total memory:64M max memory:1010M free memory:61M 注意: 本例的运行结果在不同机器上有所不同。另外,本例还使用exec()方法启动了计算器进程, 5s后关闭该进程。 5.5正则表达式 正则表达式(Regular Expression)描述了一种字符串匹配模式(pattern),可以用来检查一个串是否含有某种子串、将匹配的子串替换或者从某个串中取出符合某个条件的子串等。Java提供了正则表达式校验有关类和方法。 5.5.1元字符 构造正则表达式的方法和创建数学表达式的方法一样。使用多种元字符与运算符可以将子表达式结合在一起来创建复杂的表达式。正则表达式的组件可以是单个字符、字符集合、字符范围、字符间的选择或者所有这些组件的任意组合。 正则表达式是由普通字符(例如字符a~z)以及特殊字符(称为“元字符”)组成的文字模式。模式描述在搜索文本时要匹配的一个或多个字符串。正则表达式作为一个模板,将某个字符模式与所搜索的字符串进行匹配。常见的正则表达式 字符如表5.21所示。 表5.21常见的正则表达式字符 字符说明 $匹配输入字符串的结尾位置。如果设置了RegExp对象的Multiline属性,则$也匹配'\n'或'\r'。要匹配$字符本身,请使用\$ ( )标记一个子表达式的开始和结束位置。子表达式可以获取供以后使用。要匹配这些字符,请使用\(和\) *匹配前面的子表达式零次或多次。要匹配*字符,请使用\* +匹配前面的子表达式一次或多次。要匹配+字符,请使用\+ .匹配除换行符\n之外的任何单字符。要匹配.,请使用\. [标记一个中括号表达式的开始。要匹配[,请使用\[ ?匹配前面的子表达式零次或一次,或指明一个非贪婪限定符。要匹配?字符,请使用\? \将下一个字符标记为特殊字符、原义字符、向后引用或八进制转义符。例如,'n' 匹配字符 'n'。'\n' 匹配换行符。序列 '\\' 匹配 "\",而 '\(' 则匹配 "(" ^匹配输入字符串的开始位置,除非在方括号表达式中使用,当该符号在方括号表达式中使用时,表示不接受该方括号表达式中的字符集合。要匹配^字符本身,请使用\^ {标记限定符表达式的开始。要匹配{,请使用\{ |指明两项之间的一个选择。要匹配|,请使用\| *匹配前面的子表达式零次或多次。例如,zo*能匹配"z"以及"zoo"。*等价于{0,} +匹配前面的子表达式一次或多次。例如,'zo+'能匹配"zo"以及"zoo",但不能匹配"z"。+等价于{1,} ?匹配前面的子表达式零次或一次。例如,"do(es)?"可以匹配"do" "does"中的"does" "doxy"中的"do"。?等价于{0,1} {n}n是一个非负整数。匹配确定的n次。例如,'o{2}' 不能匹配 "Bob" 中的 'o',但是能匹配"food"中的两个o {n,}n是一个非负整数。至少匹配n次。例如,'o{2,}' 不能匹配 "Bob" 中的 'o',但能匹配"foooood"中的所有 o。'o{1,}' 等价于 'o+'。'o{0,}' 则等价于 'o*' {n,m}m和n均为非负整数,其中,n≤m。最少匹配n次且最多匹配m次。例如,"o{1,3}"将匹配"fooooood"中的前三个o。'o{0,1}' 等价于 'o?'。请注意在逗号和两个数之间不能有空格 x|y匹配 x 或 y。例如,'z|food' 能匹配"z"或"food"。'(z|f)ood' 则匹配"zood"或"food" [xyz]字符集合。匹配所包含的任意一个字符。例如,'[abc]' 可以匹配 "plain" 中的 'a' [^xyz]负值字符集合。匹配未包含的任意字符。例如,'[^abc]' 可以匹配 "plain" 中的'p'、'l'、'i'、'n' [az]字符范围。匹配指定范围内的任意字符。例如,'[az]' 可以匹配 'a'~'z' 范围内的任意小写字母字符 [^az]负值字符范围。匹配任何不在指定范围内的任意字符。例如,'[^az]' 可以匹配任何不在 'a'~'z' 范围内的任意字符 \w匹配字母、数字、下画线。等价于'[AZaz09_]' \W匹配非字母、数字、下画线。等价于 '[^AZaz09_]' 基于上述正则表达式的字符说明,表5.22列出了一些常用的正则表达式。 表5.22常用的正则表达式 名称正则表达式 Email\w[\w.+]*@([AZaz09][AZaz09]+\.)+[AZaz]{2,14} URL^((https|http|ftp|rtsp|mms)?:\/\/)[^\s]+ 邮政编码\d{6} 身份证号\d{17}[\d|x]|\d{15} 国内手机号0?1(3|5|6|7|8|9)[09]{9} 6~18位大小写字母、数字、下画线组成密码^[azAZ]\w{5,17}$ 5.5.2Pattern类与Matcher类 1. Pattern类 java.util.regex.Pattern类是对正则表达式的编译表示。其用法是先将正则表达式使用Pattern类的实例编译,然后使用生成的模式创建匹配器对象,该对象可以根据正则表达式匹配任意字符序列。采用如下方式创建匹配器对象: Pattern pattern = Pattern.compile("a*b"); Matcher matcher = pattern.matcher("aaaaaab"); boolean result = matcher.matches(); 表5.23列出了Pattern类的主要方法。 表5.23Pattern类的主要方法 方法名说明 String[] split(CharSequence input, int limit)将给定的输入序列分成这个模式的匹配,有limit参数时,表示只匹配前limit(不含)次 Matcher matcher(CharSequence input)提供了对正则表达式的分组支持,以及对正则表达式的多次匹配支持 static boolean matches(String regex, CharSequence input)编译给定的正则表达式并尝试将给定的字符序列与之匹配 2. Matcher类 java.util.regex.Matcher类用于在给定的Pattern实例的模式控制下进行字符串的匹配工作。Matcher对象由Pattern类的matcher()方法创建,表5.24列出了Matcher类的 主要方法。 表5.24Matcher类的主要方法 方法名说明 boolean matches()对整个字符串进行匹配,只有整个字符串均匹配才返回true boolean lookingAt()对前面的字符串进行匹配,只有匹配到的字符串在最前面才返回true boolean find()对字符串进行匹配,匹配到的字符串可以在任意位置 int end()返回最后一个字符匹配后的偏移量 String group()返回匹配到的子字符串 int start()返回匹配到的子字符串在字符串中的索引位置 下面的程序使用Pattern类和Matcher类演示校验电子邮件、手机号、密码、邮编、日期等典型正则表达式的应用。 【例5.16】Example5_16.java import java.util.regex.Matcher; import java.util.regex.Pattern; public class Example5_16 { public static void main(String[] args) { //邮编 String postCodeRegex = "\\d{6}"; //手机 String telRegex = "1(3|5|6|7|8|9)[0-9]{9}"; //E-mail String emailRegex = "\\w[-\\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\\.)+[A-Za-z]{2,14}"; //密码,以字母开头,长度为6~18,只能包含字母、数字和下画线 String passwordRegex = "^[a-zA-Z]\\w{5,17}$"; //日期格式 String dateRegex = "^\\d{4}-\\d{1,2}-\\d{1,2}"; String post = "475006"; //1. 使用Pattern和Matcher类校验 Pattern p1 = Pattern.compile(postCodeRegex); Matcher m1 = p1.matcher(post); boolean r1 = m1.matches(); System.out.println(r1); String tel = "14603711234"; Pattern p2 = Pattern.compile(telRegex); Matcher m2 = p2.matcher(tel); boolean r2 = m2.matches(); System.out.println(r2); String password = "Yq9zxV0"; Pattern p3 = Pattern.compile(passwordRegex); Matcher m3 = p3.matcher(password); boolean r3 = m3.matches(); System.out.println(r3); String date = "2021-11-12"; Pattern p4 = Pattern.compile(dateRegex); Matcher m4 = p4.matcher(date); boolean r4 = m4.matches(); System.out.println(r4); //2.也可使用String类的matches()方法校验 String email = "zhangsan@henu.edu.cn"; boolean r6 = email.matches(emailRegex); System.out.println(r3); String str= "电话:3316889,地址:人民路36号,门牌号10#楼东单元601房间"; Pattern p5 = Pattern.compile("\\d+"); //将str中的数字进行分隔 String[] s = p5.split(str); for(String t: s){ System.out.println(t); } } } 运行结果: true false true true true 电话: ,地址:人民路 号,门牌号 #楼东单元 房间 注意: String类也提供了matches()方法,可以应用特定正则表达式对字符串进行校验,例5.16的Email校验便是使用此方式进行校验。 5.6集合 数组有两个重要特征: 定长; 数组中的元素数据类型均一致。正是数组的这两个特征,在程序开发中也带来了不便之处,程序开发人员必须预先判定出要处理的数据数量及其数据类型,否则便无法定义数组。同时由于程序运行时的未知性因素较多,对于声明的数组长度,也有可能产生数组越界或者是声明数组的长度远远大于实际的元素个数,从而导致内存的浪费。 Java中的容器类能够有效解决数组的上述弊端。容器是一种非常实用的工具类,在java.util工具包中。它可以存储不同数据类型的元素,并且其容量可以变化。Java的容器可以大致分为两类: 单列结构Collection和双列结构Map。其中,Collection又分为两类: List和Set。List表示有序、重复的集合,Set表示无序、不可重复的集合。Map则是一种具有映射关系的集合。 5.6.1集合概述 集合类可存储不同数据类型的对象,并且集合的容量可以动态改变。Java的集合主要由两个接口派生出来: Collection和Map。其中,Collection是单列结构,Map是双列结构,由Key和Value(键值对)组成。Collection和Map是Java集合层次关系中的两个根接口,这两个接口又派生大量子接口及其实现类。图5.1描述了Java集合的层次关系。 图5.1Java集合的层次关系 在图5.1中给出了容器类层次结构关系,其中,Collection是一组独立的元素,并且这些元素服从某种规则,如List必须保持元素特定的顺序,即存入的顺序与取出的顺序一致; 而Set对象不能有重复的元素,元素存入的顺序与取出顺序也不一定相同; Map表示一种映射关系,Map是由键(Key)与值(Value)组成的映射关系,即Map是双列结构,且键不允许重复,从某种意义上来讲,Map就像数据库的数据字典。图5.2给出了一些重点集合类型的层次结构及其特点,也是本节重点介绍的内容。 图5.2主要集合类型及其特点 注意: 集合是一种长度可变、可存储不同数据类型的对象。按照存储结构包括两种类型: 单列集合Collection和双列集合Map。 Collection: 单列集合的顶层父接口,用于存储一系列符合某种规则的元素。List和Set是Collection其中两个应用最为广泛的子接口,List元素有序且可重复,Set元素无序且不能重复。 Map: 双列集合的顶层父接口,由键(Key)和值(Value)两列组成。每个元素都包括一对键值,其中键的取值必须唯一。在Map集合中,可以通过键找到对应的值。 5.6.2Collection接口 Collection作为单列集合的顶层父接口,提供了一些基础的公共方法,通过这些方法可以操作所有类型的单列集合。Collection接口定义了操作集合元素的常用方法,具体见表5.25。 表5.25Collection的常用方法 方法名说明 boolean add(E o)将对象o添加到此集合中 boolean addAll(Collection c)将容器对象c中的所有元素添加到此集合中 void clear()删除此集合中的所有元素 boolean equals(Object o)比较此集合与o是否相等 boolean isEmpty()判断此集合是否为空集合 boolean contains(Object o)判断此集合是否包含值为o的元素 Iterator iterator()返回一个迭代器,用来访问容器中的各个元素 boolean remove(Object o)如果此集合中有与o相匹配的元素,则删除此元素 boolean removeAll(Collection c)删除此集合中那些也包含在指定 collection 中的所有元素 boolean retainAll(Collection c)从此集合中删除容器对象c中不包含的元素 Object[] toArray()返回一个内含此集合所有元素的数组 5.6.3Iterator接口 java.util.Iterator接口称为迭代器,它也是Java集合框架的成员,Iterator主要用来遍历并访问容器类的元素,因此Collection集合实现了Iterator接口,甚至List接口,还支持双向遍历的ListIterator。使用Iterator遍历集合对象并不需要程序开发人员了解集合对象的底层结构,并且创建Iterator的代价较小,故Iterator被称为“轻量级”的对象。Iterator接口中具体的 常用 方法见表5.26。 表5.26Iterator的常用方法 方法名说明 boolean hasNext()判断集合中是否还有元素,如有返回true,否则返回false E next()获得集合中的下一个元素 void remove()将迭代器新返回的元素删除 注意: 集合遍历的方式有以下五种。 基本循环如for/while循环: 这种方式功能上最为强大,遍历时也可以修改、删除元素。 Iterator: 比较简便的遍历方式,遍历时可以删除元素。 ListIterator: Iterator的子接口,专门用于List集合的遍历,支持双向遍历。 Enumeration: 遍历Properties等双列集合的方式,遍历时不能修改、删除元素。 foreach: 遍历方式上最为简便,同时功能上也最弱,遍历时不能修改、删除元素。 除了基本循环之外,其他方式遍历时,不可以通过集合对象的方法操作集合中的元素,因为会发生ConcurrentModificationException异常。Iterator提供的方法是有限的,只能对元素进行判断、取出、删除的操作,如果想要其他的操作如添加、 修改等,就需要使用其子接口ListIterator。该接口只能通过List集合的listIterator方法获取。Enumeration和foreach方式功能上更为单一,遍历时不能修改、删除元素。 5.6.4List接口 List接口继承Collection接口,List接口是一种允许有重复元素的有序集合。List接口添加了面向位置的操作,允许用户对集合中每个元素的插入位置进行精确的控制,还可以根据元素的索引值访问元素,并搜索列表中的元素。表5.27列出了List接口的常用方法。 表5.27List的常用方法 方法名说明 boolean add(E o)将对象o追加到此集合的尾部 boolean add(int index, E element)将对象element添加到此向量索引为index处 boolean addAll(Collection c)将集合对象c中的全部元素追加到此集合的尾部 boolean addAll(int i,Collection c)将集合对象c中的全部元素添加到此集合索引值为i处 Object[] toArray()将此集合转换为数组 E get(int index)取该集合索引为index的元素值 E set(int index, E element)用指定的元素替换此集合中索引为index处的元素 boolean remove(int index)删除此集合中索引为index的元素 boolean remove(Object o)删除此集合中元素值为o的元素 void removeAll (Collection c)从此集合中删除c的全部元素 boolean contains(Object obj)测试obj是否为此集合中的元素,如是返回true,否则返回false boolean equals(Object obj)比较此集合与obj是否相等,如相等返回true,否则返回false List subList(int from, int to)以List形式返回此集合的部分元素,元素范围为[from,to)。如果 from和to相等,则返回的 List 为空 int size()返回此集合的大小 void clear()清除此集合的所有元素 int indexOf(Object obj)返回此集合中首次出现obj的索引值 Iterator iterator()返回此集合的迭代器 Object[] toArray()将集合转换为数组形式返回 下面分别简要介绍List接口的两个实现类ArrayList类和LinkedList类。 1. ArrayList类 ArrayList类封装了一个可动态改变大小的Object类型的数组,每个ArrayList都有一个表示其自身容量的数值,从某种意义上讲,ArrayList就是一种特殊的数组,但是效率没有数组高,ArrayList可以添加、删除和修改元素,并且其大小可动态改变。除了表5.27中的一些方法,ArrayList还提供了一些方法,详见表5.28。 表5.28ArrayList类的主要方法 方法名说明 void ensureCapacity(int minCapacity)将此ArrayList对象的容量增加minCapacity void trimToSize()将此ArrayList对象的容量调整为列表的当前实际大小 例5.17是一个有关ArrayList的实例。 【例5.17】Example5_17.java import java.util.ArrayList; import java.util.Iterator; import java.util.ListIterator; public class Example5_17 { public static void main(String[] args) { ArrayList list = new ArrayList(); //添加元素 list.add("zero"); list.add("one"); list.add("two"); list.add("three"); //集合中可以添加任意类型的元素 list.add(4); //可添加重复元素,在索引为3的位置插入 list.add(3,"one"); //直接打印集合list中的元素 System.out.println("集合中的元素: " + list ); int pos = list.indexOf("one"); System.out.println("one首次出现的位置: " + pos); System.out.println("集合的元素个数: " + list.size()); System.out.println("索引值为2的元素: " + list.get(2)); System.out.println("============"); //使用for循环遍历集合 for(int i=0;i<list.size();i++){ System.out.print(list.get(i) + ""); } System.out.println("\n============"); //使用foreach遍历集合 for(Object obj: list) System.out.print(obj + ""); System.out.println("\n============"); //使用Iterator遍历 Iterator iterator = list.iterator(); while (iterator.hasNext()){ System.out.print(iterator.next() + ""); } System.out.println("\n============"); //使用ListIterator遍历 ListIterator listIterator = list.listIterator(6); while (listIterator.hasPrevious()){ System.out.print(listIterator.previous() + ""); } } } 运行结果: 集合中的元素: [zero, one, two, one, three, 4] one首次出现的位置: 1 集合的元素个数: 6 索引值为2的元素: two ============ zeroonetwoonethree4 ============ zeroonetwoonethree4 ============ zeroonetwoonethree4 ============ 4threeonetwoonezero 通过本例可以验证: 集合中可以存放任意类型的元素,集合长度也是变长的。当元素存入集合后,元素的数据类型就转换为Object类型; 同理,从集合中取出元素时,其数据类型仍然为Object类型。因此,如果元素取出后恢复原有类型,必须使用强制类型转换。 注意: 从集合中取出元素并进行强制类型转换将给程序带来安全隐患,在集合对象实例中使用泛型是一种有效解决方案,例如: ArrayList<String>list = new ArrayList<String>(); 那么,集合list只能存放String类型的元素,其他数据类型的元素添加到该集合时将报错。从该集合中取出的元素,其数据类型是String类型,而不再是Object类型。 例5.17提供了四种遍历集合的方法,分别是使用for循环、foreach、Iterator和ListIterator。特别是ListIterator还提供了从前向后遍历(使用hasNext()和next()方法)和从后向前遍历(使用hasPrevious()和previous()方法)。 注意: ArrayList提供的listIterator()方法进行了重载,以支持以下两种遍历方式。 listIterator(): 默认从索引值为0的位置开始遍历,此时调用hasPrevious()方法返回false,即不能向前遍历,只能向后遍历。 listIterator(int index): 从指定索引处遍历,若index小于集合长度,可以向前或向后遍历。 使用Iterator迭代器遍历集合并删除其中元素时,一定要使用Iterator对象的remove()方法删除指定元素,而不能使用集合的remove()方法删除,如例5.18所示。 【例5.18】Example5_18.java import java.util.ArrayList; import java.util.Iterator; public class Example5_18 { public static void main(String[] args) { ArrayList<String> list = new ArrayList<String>(); list.add("one"); list.add("two"); list.add("three"); list.add("four"); list.add("five"); Iterator<String> iterator = list.iterator(); while (iterator.hasNext()){ String value = iterator.next(); if("three".equals(value)) list.remove("three"); System.out.print(value + ""); } System.out.println(); System.out.println("删除后的结果: " + list);} } 该程序执行时产生ConcurrentModificationException,即并发修改异常,出现异常的原因是集合在迭代器运行期间删除了元素,导致迭代器预期的迭代次数发生改变,从而使迭代器的结果不正确。因此,程序中的list.remove("three"); 这一行代码需要进行修改。如果需要在集合的迭代期间对集合中的元素进行删除,可以使用迭代器Iterator自身提供的remove()方法进行删除,将例5.18的该行程序替换为下行代码,即可解决该问题。 iterator.remove(); 运行结果: onetwothreefourfive 删除后的结果: [one, two, four, five] 2. LinkedList类 由于LinkedList类充当了动态数组,并且在创建它时不必像ArrayList那样指定其大小,因此当动态添加或删除元素时,LinkedList集合的大小将自动调整。而且,其内部元素不是以连续的方式存储的。LinkedList实现了一个双向链表结构(如图5.3所示),链表中的每一个元素都使用引用的方式来记住它的前一个元素和后一个元素,从而将所有的元素彼此连接起来。当前插入一个新元素时,只需要修改元素之间的这种引用关系即可,同理,删除一个元素也是如此。因此LinkedList提供了一些处理集合首尾两端元素的方法。使用LinkedList集合进行元素的增加或删除操作时效率很高。 图5.3双向链表结构 表5.29给出了LinkedList类的常用方法。 表5.29LinkedList类的常用方法 方法名说明 void addFirst(E o)将o插入此集合的开头 void addLast(E o)将o追加到此集合的结尾 E removeFirst()删除并返回此集合的第一个元素 E removeLast()删除并返回此集合的最后一个元素 E peek()找到但不删除此集合的头 E poll()找到并删除此集合的头 boolean offer(E o)将指定元素添加到此集合的末尾 注意: 以下情况建议使用ArrayList。 频繁访问列表中的某一个元素; 只需在列表末尾进行添加和删除元素操作。 以下情况建议使用LinkedList。 频繁地在列表首部、中间和末尾等位置进行添加和删除元素操作; 需要通过循环迭代来访问集合中的某些元素。 总之,与 ArrayList 相比,LinkedList 的增加和删除的操作效率更高,而查找和修改的操作效率较低。 下面是一个LinkedList类的实例。 【例5.19】Example5_19.java import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedList; public class Example5_19 { public static void main(String[] args) { LinkedList student = new LinkedList(); //向student的尾部追加Jimmy student.add("Jimmy"); //向student的尾部追加Eric student.offer("Eric"); student.offer("Tom"); //向student的头部追加John student.addFirst("John"); Iterator it = student.iterator(); //遍历student System.out.print("LinkedList的所有元素: "); while(it.hasNext()) { System.out.print(it.next() + " "); } System.out.println(); //打印第一个元素 System.out.println("第1个元素: " + student.getFirst()); System.out.println("peekLast后的最后1个元素: " + student.peekLast()); //访问并不删除第一个元素 System.out.println("peekFirst第1个元素: " + student.peekFirst()); //访问并删除第一个元素 System.out.println("poll第1个元素: " +student.poll()); System.out.println("删除后的所有元素为: " + student); ArrayList list = new ArrayList(); long s1 = System.currentTimeMillis(); for(int i =0;i<100000;i++){ student.add(0,i); } long s2 = System.currentTimeMillis(); System.out.println("LinkedList执行插入元素消耗时间: " + (s2-s1)); long e1 = System.currentTimeMillis(); for(int i =0;i<100000;i++){ list.add(0,i); } long e2 = System.currentTimeMillis(); System.out.println("ArrayList执行插入元素消耗时间: " + (e2-e1)); } } 运行结果: LinkedList的所有元素: John Jimmy Eric Tom 第1个元素: John peekLast后的最后1个元素: Tom peekFirst第1个元素: John poll第1个元素: John 删除后的所有元素为: [Jimmy, Eric, Tom] LinkedList执行插入元素消耗时间: 37 ArrayList执行插入元素消耗时间: 1829 通过本例可以看到,批量插入多个元素时,LinkedList要比ArrayList效率高出很多。同时,对于peekFirst()方法和poll()方法的区别,peekFirst()方法是获取LinkedList集合中的第一个元素; 而poll()方法也是获取LinkedList集合中的第一个元素,并且在集合中删除该元素。 注意: 在实际应用中,究竟选取哪种List类型的集合? 实现List接口的实现类有两个: ArrayList和LinkedList。选取哪一种取决于特定的需要。如果要支持随机访问,而不必在除尾部的任何位置插入或删除元素,那么选用ArrayList; 如果要频繁地从列表的中间位置插入或删除元素,并且只要求以顺序的方式访问列表元素,那么最好选择LinkedList。 5.6.5Set接口 Set接口也继承自Collection接口,但与List接口相比,Set接口不允许集合中存在重复项,每个具体的Set实现类依赖添加对象的equals()方法来检查其唯一性。Set接口继承了Collection接口中的方法,没有添加新的方法,在此就不再重复列出Set接口的方法。Set接口可以分为两种类型: 一种是使元素自动保持升序的集合SortedSet接口; 另一种是HashSet类及其子类LinkedHashSet。 1. HashSet类及LinkedHashSet类 实现Set接口的实现类有HashSet类和TreeSet类(TreeSet同时也实现了SortedSet接口),以及HashSet类的子类LinkedHashSet。需要注意的是,HashSet集合中的元素先后顺序并不是固定不变的。一般来说,基于效率方面的考虑,添加到HashSet中的对象需要采用恰当分配哈希码的方式对Object类的hashCode()方法进行重写。 1) HashSet类 HashSet类主要用来快速查找集合中的元素,HashSet基于对象的散列值来确定元素在集合中的存储位置,因而具有较好的存取和查找性能。又由于Set集合中的元素不能有重复值,因此,存入HashSet的元素必须重写hashCode()方法,以此验证元素是否为重复元素。表5.30列出HashSet类的构造方法和常用方法。 表5.30HashSet类的构造方法和常用方法 方法名说明 HashSet()构造一个新的空集合,默认初始容量为16,加载因子为0.75 HashSet(Collection c)构造一个包含c中的元素的新集合 HashSet(int capacity,float Factor)构造一个空集合,初始容量为capacity,加载因子为factor HashSet(int capacity)构造一个空集合,初始容量为capacity,加载因子为0.75 boolean add(E o)如果此集合中还不包含o,则添加指定元素 boolean contains(Object o)如果此集合包含o,则返回 true int size()返回此集合中的元素的数量(集合的容量) HashSet集合之所以能够确保不出现重复元素,是因为它在存入元素时做了一些计算与判断。当调用其add()方法存入元素时,首先调用当前存入元素的hashCode()方法获得对象的散列值,然后根据对象的散列值计算出存储位置。如果该存储位置上没有元素,则直接将元素存入。如果该存储位置上有元素存在,则会调用该元素的equals()方法比较将要存入的对象,如果返回值为false,则存入; 否则说明二者值重复,将要存入的对象舍弃。HashSet存入元素的工作流程如图5.4所示。 图5.4HashSet存入元素的工作流程 下面通过一个例子介绍HashSet类的基本应用,了解一些常用方法的使用及其集合的遍历方法。 【例5.20】Example5_20.java import java.util.HashSet; import java.util.Iterator; public class Example5_20 { public static void main(String[] args) { HashSet set = new HashSet(); //添加下面的6个元素,注意有重复值 set.add("white"); set.add("red"); set.add("blue"); set.add("pink"); set.add("orange"); set.add("green"); //添加重复的元素 set.add(new String("blue")); System.out.println(set); System.out.println("容量: " + set.size()); System.out.println("包含red吗?" + set.contains("red")); //遍历Set集合 Iterator it = set.iterator(); while (it.hasNext()) System.out.print(it.next() + " "); } } 运行结果: [red, orange, pink, green, white, blue] 容量: 6 包含red吗?true red orange pink green white blue 通过本例可以看到,向HashSet集合中添加String类型的元素,其重复值的元素如blue不再重复添加。原因在于String类重写了hashCode()和equals()方法,对于字符串常量和创建String对象,若字符串内容相等,二者的散列值也是相等的,两个字符串也相等。因此,字符串“blue”不能重复存入HashSet集合。 那么,HashSet集合存入其他引用类型的对象时,如何判断元素是否为重复项呢?这就要求存入的引用类型必须重写hashCode()与equals()方法。以Student类为例,假设将若干Student对象存入HashSet集合中,如果Student对象的学号与姓名均一致,则 认为是相等的,下面通过一个程序看看能否存入相同的学生对象。 【例5.21】Example5_21.java import java.util.HashSet; public class Example5_21 { public static void main(String[] args) { HashSet<Student> set = new HashSet<Student>(); Student s1 = new Student("201506899","zhangsan"); Student s2 = new Student("201506890","lisi"); Student s3 = new Student("201506893","wangwu"); Student s4 = new Student("201506899","zhangsan"); System.out.println("s1 hashCode:" + s1.hashCode()); System.out.println("s4 hashCode:" + s4.hashCode()); set.add(s1); set.add(s2); set.add(s3); set.add(s4); System.out.println(set); } } class Student{ String no; String name; public Student(String no, String name){ this.no = no; this.name = name; } @Override public String toString(){ return "{No. " + no + ", Name: " + name + "}"; } } 运行结果: s1 hashCode:1078694789 s4 hashCode:931919113 [{No. 201506899, Name: zhangsan}, {No. 201506893, Name: wangwu}, {No. 201506899, Name: zhangsan}, {No. 201506890, Name: lisi}] 本例的运行结果表明对于学号和姓名均相同的两个Student对象s1和s4,却仍然能够存入 HashSet集合中。s1和s4的散列值表明了二者不相同,因此HashSet集合能够同时存入s1和s4对象。针对该问题,如何解决呢?解决方案是重写Student类的hashCode()和equals()方法。这里给出完善Student类的代码,主类无须做修改。 class Student{ String no; String name; public Student(String no, String name){ this.no = no; this.name = name; } @Override public String toString(){ return "{No. " + no + ", Name: " + name + "}"; } //重写hashCode()方法,返回学号与姓名的散列值之差 @Override public int hashCode() { return this.no.hashCode() - this.name.hashCode(); } //重写equals()方法,如果学号no和姓名name均相等,返回true @Override public boolean equals(Object obj) { if(obj instanceof Student){ if(this.no.equals(((Student) obj).no) && this.name.equals(((Student) obj).name)) return true; } return false; } } 在Student类中,重写了hashCode()与equals()方法。hashCode()方法返回的是学号与姓名的散列值之差; equals()方法则比较学号和姓名依次相等时返回true。重新运行例5.21,HashSet集合中将不会存储重复的学生对象了。 2) LinkedHashSet类 LinkedHashSet类扩展了HashSet类,LinkedHashSet类增加了跟踪添加到HashSet中的元素顺序的功能。LinkedHashSet的迭代器按照元素的插入顺序来访问各个元素,它提供了一个可以快速访问各个元素的有序集合。当然也增加了实现的代价,因为哈希表元中的各个元素是通过双重链接式列表链接在一起的。LinkedHashSet类的主要方法见表5.31。 表5.31LinkedHashSet类的主要方法 方法名说明 LinkedHashSet()构造一个空集合,默认初始容量为16,加载因子为0.75 LinkedHashSet(Collection c)构造一个包含c中的元素的集合 LinkedHashSet(int Capacity,float Factor)构造一个初始容量为Capacity,加载因子为Factor的空集合 LinkedHashSet(int Capacity)构造一个初始容量为Capacity,加载因子为0.75的空集合 【例5.22】Example5_22.java import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Set; import java.util.TreeSet; public class Example5_22 { public static Set fill(Set s, int size) { for (int i = 0; i < size; i++) { s.add(new MySet(i)); } return s; } public static void set(Set c) { fill(c,6); c.addAll(fill(new TreeSet(),6)); System.out.println(c); } public static void main(String[] args) { System.out.print("HashSet:"); set(new HashSet()); System.out.print("LinkedHashSet:"); set(new LinkedHashSet()); } } class MySet implements Comparable { private int s; public MySet(int s) { this.s = s; } public boolean equals(Object obj){ return (obj instanceof MySet) && (s== ((MySet)obj).s); } public String toString() { return s + " "; } public int hashCode() { return s; } public int compareTo(Object obj) { int t = ((MySet)obj).s; return (t<s ? -1:(t==s ? 0 : 1)); } } 运行结果: HashSet:[0 , 1 , 2 , 3 , 4 , 5 ] LinkedHashSet:[0 , 1 , 2 , 3 , 4 , 5 ] 2. SortedSet接口及TreeSet类 SortedSet接口能够使集合保持其元素为升序顺序,将元素添加到SortedSet接口的实现类TreeSet的实例中,无论元素添加的先后顺序如何,在集合对象中总是使这些元素保持为升序顺序。TreeSet是SortedSet接口的唯一实现类。 TreeSet基本上是一个自平衡二叉搜索树(如红黑树)的实现。因此,像添加、删除和搜索这样的操作需要O(logN)时间。原因是在自平衡树中,确保所有操作的树高度始终为O(logN)。因此,这被认为是存储海量排序数据并对其执行操作的最有效的数据结构之一。但是,像按排序顺序打印N个元素这样的操作,其时间复杂度为O(N)。 下面简要介绍一下TreeSet的主要方法,详见表5.32。 表5.32TreeSet的主要方法 方法名说明 Comparator comparator()返回此集合的Comparator,或者返回null,表示以自然方式排序 E first()返回此集合的第一个元素 E last()返回此集合的最后一个元素 SortedSet headSet(E toElement)返回此集合的子集,由小于toElement的元素组成 SortedSet tailSet(E fromElement)返回此集合的子集,由大于或等于fromElement的元素组成 SortedSet subSet(E from,E to)返回此集合的子集,范围为[from, to] E lower(E e)返回小于给定元素值e的最近元素,如不存在,返回null E higher(E e)返回大于给定元素值e的最近元素,如不存在,返回null E floor(E e)返回小于或等于给定元素值e的最近元素,如不存在,返回null E ceiling(E e)返回大于或等于给定元素值e的最近元素,如不存在,返回null 【例5.23】Example5_23.java import java.util.SortedSet; import java.util.TreeSet; public class Example5_23 { public static void main(String[] args) { //创建一个SortedSet集合set SortedSet set =new TreeSet(); //添加下面的6个元素,注意有重复值 set.add("white"); set.add("red"); set.add("blue"); set.add("orange"); set.add("green"); set.add(new String("blue")); //输出集合 System.out.println("集合的内容: " + set); System.out.println("第一个元素: " + set.first()); System.out.println("最后一个元素: " + set.last()); System.out.println("subSet(1,3): " + set.subSet("green","white")); set.remove("orange"); System.out.println("删除orange元素后: " + set); } } 运行结果: 集合的内容: [blue, green, orange, red, white] 第一个元素: blue 最后一个元素: white subSet(1,3): [green, orange, red] 删除orange元素后: [blue, green, red, white] 从本例中可以看出,TreeSet集合能够自动对添加的元素进行升序排序,这是因为添加的元素类型均为String类型,String类实现了Comparable接口。Comparable接口强行对实现它的每个类的对象进行整体排序,Comparable接口的compareTo(Object obj)方法是实现排序的核心方法。 注意: 基本数据类型对应的包装类、String类都实现了Comparable接口,这意味着向TreeSet集合中添加这些类型的元素时,TreeSet集合将自动以这些类型的元素按自然排序的方式进行排序。 假设向TreeSet集合中添加其他引用类型的元素,那么这些类型的元素必须实现Comparable接口,并重写compareTo(Object obj)方法定义该引用类型的排序方式,否则TreeSet集合无法对这些对象进行排序,从而产生ClassCastException异常。如例5.24 所示,向TreeSet集合中添加Student对象。 【例5.24】Example5_24.java import java.util.TreeSet; public class Example5_24 { public static void main(String[] args) { TreeSet<Integer> set = new TreeSet<Integer>(); set.add(23); set.add(56); set.add(-96); set.add(863); set.add(56); System.out.println(set); //向TreeSet集合中添加Student对象 TreeSet<Student> ts = new TreeSet<Student>(); Student s1 = new Student("201805323","zhangsan"); Student s2 = new Student("201805324","lisi"); Student s3 = new Student("201805325","wangwu"); Student s4 = new Student("201805323","zhangsan"); ts.add(s1); ts.add(s2); ts.add(s3); ts.add(s4); System.out.println(ts); } } 如果仍然以例5.21中的Student类创建学生对象,并添加到TreeSet集合。程序运行时可以Integer类型的元素进行排序,但添加Student对象时将产生异常。因此,需要对Student类进一步完善,让Student类实现Comparable接口并重写compareTo()方法。改进后的Student类如下。 class Student implements Comparable<Student>{ String no; String name; public Student(String no, String name){ this.no = no; this.name = name; } @Override public String toString(){ return "{No. " + no + ", Name: " + name + "}"; } //重写hashCode()方法,返回学号与姓名的散列值之差 @Override public int hashCode() { return this.no.hashCode() - this.name.hashCode(); } //重写equals()方法,如果学号no和姓名name均相等,返回true @Override public boolean equals(Object obj) { if(obj instanceof Students){ if(this.no.equals(((Students) obj).no) && this.name.equals(((Students) obj).name)) return true; } return false; } //重写compareTo()方法,比较学号no是否相等,如相等再比较姓名name @Override public int compareTo(Student o) { int num = this.no.compareTo(o.no); return num == 0 ? this.name.compareTo(o.name) : num; } } 重新运行例5.24,此时程序可以正常运行,执行结果如下。可以看到无论是Integer类型的元素,还是Student类型的元素,均实现了排序且没有重复元素添加到集合中。 [-96, 23, 56, 863] [{No. 201805323, Name: zhangsan}, {No. 201805324, Name: lisi}, {No. 201805325, Name: wangwu}] 5.6.6Map接口 Map用于保存具有映射关系的数据,因此Map中每个元素由键(Key)和值(Value)两列数据组成,键和值是一对一的映射关系。Map就像数据库中的数据表,而键就像数据表的主键,通过唯一的键就能找到相对应的值。Map接口 的常用方法 如表5.33所示。 表5.33Map接口的常用方法 方法名说明 void clear()从此Map中移除所有映射关系 boolean containsKey(Object key)如果此Map包含key,则返回 true boolean containsValue(Object value)如果此Map包含指定value映射到一个或多个键,则返回 true V get(Object key)返回此Map中映射到键为key的值 V put(K key,V value)将值value与此Map中的键key相关联 V remove(Object key)如果存在键值为key的映射关系,则将其从映射中移除 void putAll(Map K)把指定Map中的所有映射关系复制到此映射中 Set keySet()返回此Map中所有键值组成的Set集合 Collection values()返回此Map里所有值即Value组成的Collection集合 Set entrySet()返回此Map中包含的键值对所组成的Set集合 boolean equals(Object o)比较指定的对象与此Map是否相等 注意: Map中键和值都可以为null,但是键必须是唯一的,使用put()方法添加一对映射关系时,如果映射关系中的键已经存在,那么与此键相关的新值将取代旧值。 1. HashMap类及其LinkedHashMap类 HashMap是Map接口的典型实现类,HashMap自JDK 1.2以来成为Java集合的一部分,该类位于java.util包中。它提供了Java映射接口的基本实现。它以“键值”对的形式存储数据,我们可以通过键找到其对应的值,如果尝试插入重复键,它将替换相应键的元素。 HashMap类似于HashTable,但它是不同步的。它也允许存储空键,但是应该只有一个空键对象,但可以有任意数量的空值。此类不保证映射的顺序,要使用此类及其方法,需要导入java.util.HashMap包或其超类。 【例5.25】Example5_25.java import java.util.*; public class Example5_25 { public static void main(String[] args) { HashMap<String,String> map = new HashMap<String,String>(); map.put("2","lisi"); map.put("1","zhangsan"); map.put("3","wangwu"); //添加一个重复key,相当于修改key对应的value map.put("3","liming"); System.out.println(map); //keySet()方式遍历map Set<String> keyset = map.keySet(); Iterator<String> it1 = keyset.iterator(); while (it1.hasNext()){ String key = it1.next(); String value = map.get(key); System.out.println(key +"-->" + value); } System.out.println("============"); //entrySet()方式遍历map,迭代成员为Map.Entry Set<Map.Entry<String,String>> entryset = map.entrySet(); Iterator<Map.Entry<String,String>> it2 = entryset.iterator(); while (it2.hasNext()){ Map.Entry<String,String> entry = it2.next(); String key = entry.getKey(); String value = entry.getValue(); System.out.println(key + "-->" + value); } System.out.println("============"); //遍历值 Collection<String> collection = map.values(); for (String value:collection){ System.out.println(value); } System.out.println("============"); System.out.println(map.containsValue("zhangsan")); System.out.println(map.containsKey(6)); } } 运行结果: {1=zhangsan, 2=lisi, 3=liming} 1-->zhangsan 2-->lisi 3-->liming ============ 1-->zhangsan 2-->lisi 3-->liming ============ zhangsan lisi liming ============ true false 本例先后使用keySet()和entrySet()两个方法返回的Iterator对象遍历HashMap对象。keySet()方法返回HashMap集合中的键并生成一个Set对象,然后使用迭代器遍历该Set对象; entrySet()方法将HashMap每个“键值”对作为整体封装成Map.Entry对象,转存至Set集合中,然后使用迭代器遍历。 通过本例可以发现遍历HashMap集合迭代出来的元素顺序与存入的顺序是不一致的。如果想让这两个顺序一致,可以使用LinkedHashMap类,它是HashMap的子类,与LinkedList一样,它也使用双向链表来维护内部元素的关系,使Map元素迭代的顺序与存入的顺序一致。 如果在Map中插入、删除和定位元素,HashMap是最佳选择。需要注意的是,使用HashMap类时要求添加的键(Key)要明确重写hashCode()和equals()两个方法。此外,HashMap类的构造方法中有两个重要参数初始容量(initialCapactiy)和负载因子(loadFactor)。容量是指哈希表中桶(bucket)的数量,而初始容量只是哈希表在创建时的容量。负载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,负载因子为0时表示为空的哈希表,为0.5时表示半满的哈希表。负载因子的默认值为0.75,以寻求在时间和空间上达到折中,如果负载因子过高,虽然减少了空间开销,但同时也增加了查询成本。 2. SortedMap接口及其实现类TreeMap SortedSet接口有一个实现类TreeSet,与之相似,SortedMap接口也有一个实现类TreeMap。同理,TreeMap对该映射关系中的所有键进行排序,从而保证TreeMap中所有的映射关系保持有序状态。特别注意,添加到SortedMap对象的映射关系必须实现Comparable接口,否则必须给它的构造方法提供一个Comparator接口的实现。表5.34列出了SortedMap接口的常用方法。 表5.34SortedMap接口的常用方法 方法名说明 Comparator comparator()返回对关键字排序时使用的比较器 Object firstKey()返回映射关系中第一个键 Object lastKey()返回映射关系中最后一个键 SortedMap subMap(Object beginKey, Object endKey)返回(beginKey,endKey)范围内的SortedMap子集 SortedMap headMap(Object endKey)返回SortedMap的一个视图,其内各元素的键都小于endKey SortedMap tailMap(Object beginKey)返回SortedMap的一个视图,其内各元素的键都大于或等于beginKey Collection values()以Collection形式返回此映射包含的值 TreeMap是Java集合框架的另一个重要成员,该类实现了Map接口、NavigableMap接口和SortedMap接口,同时扩展了AbstractMap类。TreeMap不允许键为null,试图将null作为键存入TreeMap集合时会引发NullPointerException异常,但是值为null却不受限制。 【例5.26】Example5_26.java import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.TreeMap; public class Example5_26 { public static void main(String[] args) { TreeMap<Integer,String> treeMap = new TreeMap<Integer,String>(); treeMap.put(12,"Melon"); treeMap.put(1,"Tom"); treeMap.put(41,"Eric"); //键重复,键为12对应的值将更新 treeMap.put(12,"Megan"); //TreeMap的键不能为null //treeMap.put(null,"none"); System.out.println(treeMap); //遍历TreeMap集合 Set set = treeMap.entrySet(); Iterator<Map.Entry<Integer,String>> iterator = set.iterator(); while (iterator.hasNext()){ Map.Entry<Integer,String>entry = iterator.next(); System.out.println(entry.getKey() + ":" + entry.getValue()); } } } 运行结果: {1=Tom, 12=Megan, 41=Eric} 1:Tom 12:Megan 41:Eric 在本例中,使用泛型对TreeMap进行了约束,键为Integer类型,值为String类型。由于Integer实现了Comparable接口,因此TreeMap集合可对键进行自然排序。同时也使用了entrySet()方法,返回一个Set集合对象,进而使用迭代器对TreeMap集合进行遍历。 TreeMap集合也可以对添加的元素的键进行排序,其实现与TreeSet一样。TreeMap排序分为自然排序和比较排序。可以让添加的元素实现Comparable接口实现自然排序,或者让TreeMap对象实现Comparator接口实现比较排序。仍以Student类为例,前面对Student类实现了Comparable接口,因此当Student对象作为键存入TreeMap集合中时,将自动按Student的自然排序方式对集合中的键值对进行排序。 【例5.27】Example5_27.java import java.util.Comparator; import java.util.TreeMap; public class Example5_27 { public static void main(String[] args) { //使用匿名内部类定义排序规则: 按姓名排序,如果姓名相同,再按年龄升序排列 TreeMap treeMap = new TreeMap(new Comparator<Student>() { @Override public int compare(Student o1, Student o2) { int num = o1.getName().compareTo(o2.getName()); return num==0 ? o1.getAge()-o2.getAge():num; } }); Student s1 = new Student("zhangsan",20); Student s2 = new Student("lisi",21); Student s3 = new Student("wangwu",19); Student s4 = new Student("zhangsan",18); treeMap.put(s1,"Kaifeng"); treeMap.put(s2,"Zhengzhou"); treeMap.put(s3,"Luoyang"); treeMap.put(s4,"Xinxiang"); System.out.println(treeMap); } } class Student {//implements Comparable<Student>{ private String name; private int age; public Student(String name,int age){ this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } public void setName(String name) { this.name = name; } public void setAge(int age) { this.age = age; } @Override public String toString() { return"[" + this.getName() + ":" + this.age + "]"; } //排序规则: 先按学生姓名升序排序,如果姓名相同,再按年龄升序排序 //@Override //public int compareTo(Student o) { //int num = 0; //num = this.getName().compareTo(o.getName()); //if(num == 0) //return this.getAge() - o.getAge(); //else //return num; //} } 运行结果: {[lisi:21]=zhengzhou, [wangwu:19]=luoyang, [zhangsan:18]=xinxiang, [zhangsan:20]=kaifeng} 本例的Student类包括两个属性name和age,在compareTo(Object obj)方法中设置了排序规则: 先按学生姓名name升序排序,如果name相同,再按年龄age升序排序。另外一种比较排序的方式是让TreeMap对象实现Comparator接口并重写compare(Object o1,Object o2)方法。本例中的代码在比较排序的方式实现,在TreeMap实例化时以匿名内部类的方式实现了Comparator接口。倘若使用自然排序的方式,可以将匿名内部类的代码注释掉,将Student类代码的注释部分加上即可。 注意: TreeMap集合对键对象排序的方式有两种: 自然排序和比较排序。 自然排序是将作为键的引用类型实现Comparable接口,并重写compareTo(Object obj)方法。以Student类为例,该类实现Comparable接口并重写相应方法。 比较排序是将TreeMap实现Comparator接口,并重写compare(Object o1, Object o2)方法。一般在TreeMap对象实例化,以匿名内部类的方式实现。 3. Properties类 java.util.Properties类称为属性列表,继承于Hashtable类。Hashtable类与HashMap比较相似,区别在于Hashtable是线程安全的,Hashtable存取元素时速度较慢,目前已基本被HashMap所取代。Properties类表示一个持久的属性列表,属性列表中的每个键及其对应值是一个字符串。Properties类被许多Java类使用,例如前面介绍的System类的getProperties()方法,其返回值就是Properties类型。 同时,Properties类还提供load()和list()方法,可以读写磁盘上的资源文件(.properties)。在实际开发中,经常使用Properties对象来存取应用的配置项。表5.35列出Properties类的常用方法。 表5.35Properties类的常用方法 方法名说明 Properties()构造一个空属性列表 String getProperty(String key)用指定的键在此属性列表中搜索属性 void list(PrintStream streamOut)将属性列表输出到指定的输出流 void load(InputStream streamIn) throws IOException从输入流中读取属性列表 Enumeration propertyNames()返回属性列表中所有键的枚举 Object setProperty(String key,String value)调用 Hashtable 的put()方法 【例5.28】Example5_28.java import java.io.*; import java.util.Enumeration; import java.util.Properties; public class Example5_28 { public static void main(String[] args) { Properties p = new Properties(); p.put("user","root"); p.put("password","secret"); p.put("url","jdbc:mysql//localhost:3306/db"); p.put("driverClassName","com.mysql.jdbc.Driver"); //将属性列表保存到磁盘上某个文件 try{ PrintStream ps = new PrintStream("c:/file.properties"); p.list(ps); ps.close(); }catch (IOException e){ e.printStackTrace(); } p.clear(); //将磁盘上资源文件读取出来,加载到Properties对象中 try{ FileReader fr = new FileReader("c:/file.properties"); p.load(fr); fr.close(); }catch (IOException e){ e.printStackTrace(); } //遍历Properties对象 Enumeration en = p.propertyNames(); while (en.hasMoreElements()){ Object obj = en.nextElement(); System.out.println(obj + "=" + p.get(obj)); } } } 运行结果: user=root url=jdbc:mysql//localhost:3306/db password=secret driverClassName=com.mysql.jdbc.Driver 5.6.7数组与容器的区别 数组和集合的差异,主要从以下几方面进行对比。 效率: 数组是一种高效的存储和访问元素的数据类型,数组中的元素在内存中以连续方式存储,通过“数组名[index]”的方式可以轻松地访问数组中的元素,但是数组一旦定义并初始化后,其长度和元素的数据类型也就固定下来不能再改变。而集合存储和访问元素需要使用专门的方法如add()、get()方法等。因此,从效率上讲,数组要比容器类的效率高。 类型: 集合不以具体的数据类型来处理对象,而是把所有的数据类型都以Object类型来处理,正是集合这种处理数据的机制,使集合可以存储不同数据类型的元素。另外,基本数据类型的元素是不能直接存放到容器中的,而是转换成相对应的包装类对象后再存放到容器中。而数组既可以保存基本数据类型也可以保存引用类型。 使用: 任何类型的元素存入集合后,其数据类型将自动转换为Object类型; 元素从集合取出时,仍然是Object类型,元素要返回原有数据类型必须进行强制类型转换,这也给程序带来了安全风险,并且效率大幅下降。正基于此,集合又引入了泛型,从而约束集合中的元素必须是某种具体类型,避免了多次类型转换。而数组的使用规则必须先声明初始化再使用,元素存入和取出都不需要进行类型转换。 综合以上对比,可以认为数组是一种“轻量级”的数据类型,使用简单方便,但是功能上略微单薄; 而集合是一种“重量级”的数据类型,它提供了丰富的接口,功能强大,性能卓越,但是使用时略显“笨重”,需要对各元素进行数据类型转换。 注意: 何时使用集合?在下列情形下建议使用集合而不使用数组。 需要处理的对象数目不定,序列中的元素都是对象或可以表示为对象; 需要将不同类型的对象组合成一个数据序列; 需要做频繁的对象序列中元素的插入和删除; 经常需要定位序列中的对象和其他查找操作; 在不同的类之间传递大量的数据; 需要一些特定的操作,如要求数据序列中不能有重复元素、自动排序等。 5.7泛型 Java泛型(generics)是JDK 1.5之后增加的一个特性,泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说,所操作的数据类型被指定为一个参数。Java集合几乎全部支持泛型,在前面介绍集合的例子中也使用了泛型。集合使用泛型后,相当于为集合增加了约束,由原来可以存储任意数据类型的元素,更改为只能存储指定数据类型的元素。 在定义类、方法、接口时引入泛型,能够使程序具有更好的普适性。假设有这样一个需求: 定义一个排序方法,能够对整型数组、字符组数组、日期类型数组等多种数据类型的数组进行排序,根据已学习的知识,很多人员想到了方法重载。但是,方法重载的缺陷也很明显,致使程序比较臃肿。泛型也可以解决该问题,并且可以做到代码极度简洁。 泛型允许程序员在使用强类型程序设计语言编写代码时定义一些可变部分,这些可变部分可以在运行前指定具体数据类型。在编程中使用泛型代替某个实际的数据类型,而后通过实际调用时传入或推导的类型来对泛型进行替换,从而达到代码复用的目的。Java中常见泛型标记符如下。 E: Element,多在集合中使用,表示存放的元素; T: Type,表示Java类; K: Key,表示键; V: Value,表示值; N: Number,表示数值类型; ?: 表示不确定的Java类型。 除了这些标记符,其实也可以是任意的字母。在使用泛型的过程中,操作数据类型被指定为一个参数,这种参数类型在类、方法、接口中,分别称为泛型类、泛型方法和泛型接口。相对于传统的形参,泛型可以使参数具有更多类型上的变化,使代码更好地复用。 5.7.1泛型类 泛型类是在传统类声明时通过使用泛型标记符表示类中某个属性的类型,或者是某个方法的返回值或形参类型。当开发人员在实例化该类的对象时,指定泛型指代的具体类型。 泛型类的声明格式具体如下。 [修饰符] class 类名<泛型标识符1, 泛型标识符2,…>{ [修饰符] 泛型标识符 属性名称; [修饰符] 泛型标识符 方法名称(泛型标识符 参数名称,…){} } 泛型类定义之后,创建该类的对象,语法格式如下。 类名<参数化类型> 对象名称 = new 类名<参数化类型>(参数列表); 例如,前面创建TreeSet对象时,引入泛型后,实例化后若所有元素均为String类型,实例化方式如下。 TreeSet<String> treeset = new TreeSet<String>(); 下面的代码定义了一个泛型类Generic,该类声明时使用了两个泛型标识符T和K。 Generic.java class Generic<T,K>{ private T value; private K key; Set<K> hashset = new HashSet<K>(); //泛型方法 public T get(){ return this.value; } //泛型方法 public void set(T t){ this.value = t; } public void print(){ System.out.println(value); } public void add(K key){ hashset.add(key); } public void list(){ Iterator<K> iterator = hashset.iterator(); while(iterator.hasNext()) System.out.print(iterator.next() + ""); System.out.println(); } } 5.7.2泛型方法 前面介绍的泛型类中已包括泛型方法,泛型方法的定义与其所在类是否为泛型类没有任何关系。泛型方法的语法格式如下 。 [修饰符] 泛型标识符 方法名称(泛型标识符 参数名称,…){} 例如,下面定义的add(K key)方法就是一个泛型方法。 public void add(K key){ hashset.add(key); } 定义泛型方法的规则如下。 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前。 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。 类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。 泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是基本数据类型(如int、double、char 等)。 5.7.3泛型接口 Java集合中的接口基本上都是泛型接口,当然,开发人员也可以自定义泛型接口,声明泛型接口与声明泛型类的语法格式类似,具体格式如下。 [修饰符] interface 接口名称<泛型标识符1, 泛型标识符2,…>{} 例如,创建一个泛型接口Service: interface Service<E>{ public E add(E a); } 【例5.29】Example5_29.java import org.junit.Test; import java.util.*; //单元测试,JUnit public class Example5_29 { @Test public void test1(){ //实例化时,<String,Integer>替换声明时的<T,K> Generic<String,Integer> generic = new Generic<>(); generic.set("hello"); generic.print(); generic.add(200); generic.add(90); generic.add(236); generic.list(); } @Test public void test2(){ Service<Integer> service = new Service<Integer>() { @Override public Integer add(Integer a) { return a + 100; } }; System.out.println(service.add(98)); } } //泛型类 class Generic<T,K>{ private T value; private K key; private Set<K> hashset = new HashSet<K>(); //泛型方法 public T get(){ return this.value; } //泛型方法 public void set(T t){ this.value = t; } public void print(){ System.out.println(value); } public void add(K key){ hashset.add(key); } public void list(){ Iterator<K> iterator = hashset.iterator(); while(iterator.hasNext()) System.out.print(iterator.next() + ""); System.out.println(); } } //泛型接口 interface Service<E>{ public E add(E a); } 运行结果: hello 20090236 198 本例定义了一个泛型类Generic和一个泛型接口Service,类和接口中又定义了若干泛型方法。在测试类的test1()和test2()方法 中分别实例化了Generic对象和Service对象。注意,为了简化操作,本例使用了JUnit单元测试的方法。 5.8Lambda表达式 Lambda表达式是JDK 8新增的特性,Lambda表达式允许把函数作为一个方法的参数,允许创建一个不属于任何类的函数。它主要实现具有唯一方法的接口(称为函数式接口),Lambda表达式可以取代大部分匿名内部类,使代码变得更加简洁紧凑。 Lambda表达式由参数列表、箭头符号(->)和函数体组成。其中,函数体既可以是一个表达式,也可以是一个语句块。Lambda表达式的语法格式如下。 (parameters) -> expression; 或 (parameters) -> {statements;}; 以下是Lambda表达式的重要特征。 可选类型声明: 不需要声明参数类型,编译器可以统一识别参数值; 可选的参数圆括号: 一个参数无须定义圆括号,但多个参数需要定义圆括号; 可选的大括号: 如果主体包含一个语句,就不需要使用大括号; 可选的返回关键字: 如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定表达式返回了一个数值。 【例5.30】Example5_30.java import org.junit.Test; public class Example5_30 { //使用正常的实现implements @Test public void test1(){ Impl i = new Impl(); i.sayHello("World!"); } //匿名内部内的形式实现 @Test public void test2(){ Greeting greeting = new Greeting() { @Override public void sayHello(String s) { System.out.println("Hello, "+ s); } }; greeting.sayHello("Eric!"); } //使用Lambda表达式的形式 @Test public void test3(){ Greeting h = (s)->{ System.out.println("Hello, "+ s); }; h.sayHello("Java!"); } } //实现Greeting接口 class Impl implements Greeting{ @Override public void sayHello(String s) { System.out.println("Hello, " + s); } } //函数式接口 interface Greeting{ public void sayHello(String s); } 运行结果: Hello, World! Hello, Eric! Hello, Java! 本例定义了一个接口Greeting,该接口中仅有一个方法sayHello()方法。而Impl类是Greeting接口的实现类。在测试类Example 5_30的三个测试方法中,test1()方法基于实现类Impl创建对象并执行Greeting的sayHello()方法; test2()方法基于匿名内部类的方式创建Greeting对象并执行sayHello()方法; test3()方法基于Lambda表达式执行Greeting接口中的sayHello()方法。对比可知,Lambda表达式的方式更加简洁。 注意: Lambda表达式实现接口中的方法,对接口有明确要求,即接口必须是函数式接口(Functional Interface)。所谓函数式接口,也就是接口中仅定义了一个抽象方法。 5.9思政案例: 保护环境,从垃圾分类做起 5.9.1案例背景 随着人们生活水平的不断提高,对生活质量的追求也愈发强烈,美丽的生态环境和绿色的生活方式自然不能缺席。同时, 随着人们生活质量的提高也产生越来越多的垃圾。如果垃圾没有妥善处理,不仅污染环境,还造成资源浪费。据有关部门统计,我国每年约有300万吨废钢铁,600万吨废纸没得到利用。而我们经常随手丢弃的废干电池,每年就有60多亿只,里面总共含有7万多吨锌,10万吨二氧化锰。这些资源如果都能被重新利用,将会成为巨大的社会财富!同时,一些有毒垃圾如果没有得到正确的分类处理,会增加填埋或焚烧的垃圾量,焚烧的垃圾越多,释放的有毒气体就越多,同时还会产生有害灰尘; 而地下填埋也会污染地下水和土壤,这些都对我们的健康构成了极大威胁。 因此,垃圾分类处理刻不容缓!垃圾分类是指按照垃圾的成分、属性、利用价值、对环境的影响以及现有处理方式的要求,分离不同类别的若干种类。欧美发达国家与国内城市的垃圾分类经验告诉我们: 垃圾分类是垃圾进行科学处理的前提,为垃圾的减量化、资源化、无害化处理奠定基础。垃圾分类处理有以下好处。 将易腐有机成分为主的厨房垃圾单独分类,为垃圾堆肥提供优质原料,生产出优质有机肥,有利于改善土壤肥力,减少化肥施用量。 将有害垃圾分类出来,减少了垃圾中的重金属、有机污染物、致病菌的含量,有利于垃圾的无害化处理,减少了垃圾处理的水、土壤、大气污染风险。 提高了废品回收利用的比例,减少了原材料的需求,减少二氧化碳的排放。 普及环保与垃圾的知识,提升全社会对环卫行业的认知,减少环卫工人的工作难度,形成尊重、关心环卫工人的氛围。 普及环保理念和垃圾分类知识,全民参与垃圾分类,养成绿色文明的生活方式,保护我们共同的生活家园。中国的垃圾分类回收还处于起步阶段,日常生活中很多民众还不知道如何将垃圾归类。通常,垃圾分为四个大类: 可回收垃圾、厨余垃圾、有害垃圾和其他垃圾,对应四个不同颜色的垃圾桶。可回收垃圾又包括废纸、塑料、玻璃、金属和布料五类等。厨余垃圾包括剩菜剩饭、骨头、菜根菜叶、果皮等食品类废物,经生物技术处理成堆肥,每吨可生产0.6~0.7t有机肥料。有害垃圾指含有对人体健康有害的重金属、有毒的物质或者对环境造成现实危害或者潜在危害的废弃物,包括电池、荧光灯管、灯泡、水银温度计、油漆桶、家电类、过期药品、过期化妆品等。这些垃圾一般使用单独回收或填埋处理。其他垃圾包括除上述几类垃圾之外的砖瓦陶瓷、渣土、卫生间废纸纸巾等难以回收的废弃物,这类垃圾采取卫生填埋可有效减少对地下水、地表水、土壤及空气的污染。 5.9.2案例任务 本案例的任务是为大众设计一个垃圾识别分类程序,根据用户丢弃的垃圾,告诉用户这是什么类型的垃圾,需要投放到什么颜色的垃圾桶。 5.9.3案例实现 分析: 本案例主要基于垃圾名称来识别垃圾所属的分类。因此,可定义四种字符串类型的常量,代表四种类型的垃圾。当用户输入某种名称的垃圾时,通过与四种字符串常量匹配,从而确定垃圾应投放到哪个垃圾桶。 【例5.31】Example5_31.java import javax.swing.*; public class Example5_31 { public static void main(String[] args) { //调用输入对话框并输入垃圾名称 String waste = JOptionPane.showInputDialog("请输入垃圾名称:"); //调用消息对话框,显示分类结果 JOptionPane.showMessageDialog(null, GarbageClassification.classfier(waste)); } } class GarbageClassification{ final static String RECYCLABLE = "报纸、期刊、图书、各种包装纸、塑料制桶、" + "制盆、制瓶、塑料衣架、玻璃瓶、碎玻璃片、镜子、暖瓶、易拉罐、罐头盒、" + "衣服、桌布、洗脸巾、书包、鞋"; final static String KITCHEN_WASTE = "剩菜剩饭、骨头、菜根菜叶、果皮、食品"; final static String HARMFULE_WASTE = "电池、荧光灯管、灯泡、水银温度计、" + "油漆桶、家电类、过期药品、过期化妆品"; final static String OTHER_WASTE = "砖瓦陶瓷、渣土、纸巾"; /** * 分类方法 * @param waste 垃圾名称 * @return 垃圾分类 */ public static String classfier(String waste){ if(RECYCLABLE.contains(waste)){ return waste + "是可回收垃圾,请投至蓝色垃圾桶!"; } else if(KITCHEN_WASTE.contains(waste)) { return waste + "是厨余垃圾,请投至绿色垃圾桶!"; } else if(HARMFULE_WASTE.contains(waste)){ return waste + "是有毒垃圾,请投至红色垃圾桶!"; } else{ return waste + "是其他垃圾,请投至灰色垃圾桶!"; } } } 运行结果如图5.5所示。 图5.5垃圾分类的运行结果 小结 本章介绍了字符串相关的类,如String、StringBuffer、StringBuilder和StringTokenizer等类的使用方法。String类主要处理不变字符串,而StringBuffer和StringBuilder主要处理可变字符串。String类对Object类的equals()方法进行了重写,用来比较两个字符串对象的内容是否相等,而StringBuffer、StringBuilder类仍继承了Object类的equals()方法,比较的是两个字符串地址是否相等。 Java提供了丰富的工具类,包括时间与日期、数值与随机数、正则表达式等,这些类是应用开发中使用非常频繁的类。 数组长度不能动态改变,数组中元素的类型也必须相同。由于这些限制,Java提供了丰富的集合对象。Java的集合包括单列集合Collection接口和双列集合Map接口,其中,Collection接口又分为List接口和Set接口等。List是一种保存有序可重复元素的集合,而Set是一种保存无序不可重复的元素集合。Map是一种映射关系,它的每一个元素包括两个对象: 键和值,它们是一种映射模型。 泛型进一步规范了集合中元素的数据类型,使用泛型定义类、接口或方法能够解决方法重载带来的代码臃肿问题。同理,Lambda表达式在很多场合下可以替代匿名内部类,使代码更加简洁。