Java高级学习笔记 学习来源:尚硅谷
学习时间:2022年3月23日
1 多线程 略,详见实用技术-Java并发编程学习笔记
2 常用类 2.1 字符串相关的类 2.1.1 概述
2.1.2 String的不可变性 通过字面量的方式(区别于new
)给一个字符串赋值,此时的字符串值声明在字符串常量池 中。
字符串常量池中不会存储相同内容的字符串。
不可变性(保护性拷贝)
当对字符串重新赋值时,需要新开辟内存区域进行赋值,不能改变原有的内存区域存储的值
当对现有的字符串进行连接操作时,也需要新开辟内存区域进行赋值
当调用String的方法修改指定的字符或字符串时,也需要新开辟内存区域进行赋值
代码示例和图演示
1 2 String s1 = "abc" ; String s2 = "abc" ;
1 2 3 String s1 = "abc" ;String s2 = "abc" ;s1 = "hello" ;
1 2 3 4 5 String s1 = "abc" ;String s2 = "abc" ;s2 += "def" ; System.out.println(s1); System.out.println(s2);
1 2 3 4 String s1 = "abc" ;String s2 = s1.replace("a" , "m" );System.out.println(s1); System.out.println(s2);
2.1.3 String对象的创建 ① 概述和使用 可以通过字面量或者new + 构造器
的方式来创建String对象:
1 2 3 4 5 6 7 8 9 10 11 12 String str = "hello" ;String s1 = new String (); String s2 = new String (String original); String s3 = new String (char [] a); String s4 = new String (char [] a,int startIndex,int count);
字面量和new的区别
字符串常量存储在字符串常量池 ,目的是共享
字符串非常量对象存储在堆 中。
代码示例1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 static void test2 () { String s1 = "abc" ; String s2 = "abc" ; String s3 = new String ("abc" ); String s4 = new String ("abc" ); System.out.println(s1 == s2); System.out.println(s1 == s3); System.out.println(s3 == s4); System.out.println(s1.equals(s3)); System.out.println(s3.equals(s4)); }
代码示例2
1 2 3 Person p1 = new Person ("Tom" , 12 );Person p2 = new Person ("Tom" , 12 );System.out.println(p1.name == p2.name);
1 2 3 4 Person p1 = new Person ("Tom" , 12 );Person p2 = new Person ("Tom" , 12 );p1.name = "Jack" ; System.out.println(p2.name);
问答题
1 String s = new String ("abc" );
问:在内存中创建了几个对象?
答:两个 ,一个是堆空间中的String对象,一个是String对象中char[]
属性对应的常量池中的数据。
② 不同拼接操作的对比
常量与常量的拼接结果在常量池,且常量池中不会存在相同内容的常量
只要拼接内容中有一个是变量,结果就在堆 中,相当于new了一个字符串
如果拼接的结果调用intern()
方法,返回值就在常量池中
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Test public void test0 () { String s1 = "javaEE" ; String s2 = "hadoop" ; String s3 = "javaEEhadoop" ; String s4 = "javaEE" + "hadoop" ; String s5 = s1 + "hadoop" ; String s6 = "javaEE" + s2; String s7 = s1 + s2; System.out.println(s3 == s4); System.out.println(s3 == s5); System.out.println(s3 == s6); System.out.println(s3 == s7); System.out.println(s5 == s6); System.out.println(s5 == s7); System.out.println(s6 == s7); String s8 = s5.intern(); System.out.println(s3 == s8); }
图示:
1 2 3 4 5 6 7 @Test public void test1 () { String s1 = "javaEEhadoop" ; final String s2 = "javaEE" ; String s3 = s2 + "hadoop" ; System.out.println(s1 == s3); }
注意:由final
修饰的是常量,即s2是常量,不是变量。因此第五句代码本质上是常量与常量的拼接。
面试题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class StringTest { String str = new String ("good" ); char [] ch = { 't' , 'e' , 's' , 't' }; public void change (String str, char ch[]) { str = "test ok" ; ch[0 ] = 'b' ; } public static void main (String[] args) { StringTest ex = new StringTest (); ex.change(ex.str, ex.ch); System.out.print(ex.str + " and " ); System.out.println(ex.ch); } }
说明:关于值传递:
基本数据类型传递的是数据本身
引用数据类型传递的是地址值
2.1.4 常用方法
int length()
:返回字符串的长度
char charAt(int index)
:返回某索引处的字符
isEmpty()
:判断是否是空字符串
String toLowerCase()
:使用默认语言环境,将 String 中的所有字符转换为小写
String toUpperCase()
:使用默认语言环境,将 String 中的所有字符转换为大写
String trim()
:返回字符串的副本,忽略前导空白和尾部空白
boolean equals(Object obj)
:比较字符串的内容 是否相同
boolean equalsIgnoreCase(String anotherString)
:与equals方法类似,忽略大小写
String concat(String str)
:将指定字符串连接到此字符串的结尾,等价于用“+”
int compareTo(String anotherString)
:比较两个字符串的大小,返回差值
String substring(int beginIndex)
:返回一个新的字符串,它是此字符串的从beginIndex开始截取到最后的一个子字符串。
String substring(int beginIndex, int endIndex)
:返回一个新字符串,它是此字符串从beginIndex开始截取到endIndex(不包含 )的一个子字符串(左闭右开 )。
boolean endsWith(String suffix)
:测试此字符串是否以指定的后缀结束
boolean startsWith(String prefix)
:测试此字符串是否以指定的前缀开始
boolean startsWith(String prefix, int toffset)
:测试此字符串从指定索引开始的子字符串是否以指定前缀开始
boolean contains(CharSequence s)
:当且仅当此字符串包含指定的 char 值序列时,返回 true
int indexOf(String str)
:返回指定子字符串在此字符串中第一次出现处的索引,找不到返回-1
int indexOf(String str, int fromIndex)
:返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始
int lastIndexOf(String str)
:返回指定子字符串在此字符串中最右边出现处的索引
int lastIndexOf(String str, int fromIndex)
:返回指定子字符串在此字符串中最后一次出现处的索引,从指定的索引开始反向搜索
String replace(char oldChar, char newChar)
:返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的
String replace(CharSequence target, CharSequence replacement)
:使用指定的字面值替换序列替换此字符串所有匹配字面值目标序列的子字符串
String replaceAll(String regex, String replacement)
: 使 用 给 定 的replacement 替换此字符串所有匹配给定的正则表达式的子字符串
String replaceFirst(String regex, String replacement)
: 使 用 给 定 的replacement 替换此字符串匹配给定的正则表达式的第一个子字符串
boolean matches(String regex)
:告知此字符串是否匹配给定的正则表达式
String[] split(String regex)
:根据给定正则表达式的匹配拆分此字符串
String[] split(String regex, int limit)
:根据匹配给定的正则表达式来拆分此字符串,最多不超过limit个,如果超过了,剩下的全部都放到最后一个元素中
代码示例1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Test public void test1 () { String s1 = "HelloWorld" ; System.out.println(s1.length()); System.out.println(s1.charAt(1 )); System.out.println(s1.isEmpty()); String s2 = s1.toUpperCase(Locale.ROOT); System.out.println(s1); System.out.println(s2); String s3 = " hello world " ; String s4 = s3.trim(); System.out.println(s3); System.out.println(s4); }
代码示例2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Test public void test2 () { String s1 = "abc" ; String s2 = "def" ; String s3 = s1.concat(s2); System.out.println(s3); String s4 = "abc" ; String s5 = new String ("abz" ); String s6 = "abc" ; System.out.println(s4.compareTo(s5)); System.out.println(s4.compareTo(s6)); String s7 = "Hongyi" ; String s8 = s7.substring(1 ); String s9 = s7.substring(1 ,3 ); System.out.println(s8); System.out.println(s9); }
代码示例3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Test public void test3 () { String s1 = "helloworld" ; boolean b1 = s1.endsWith("rld" ); boolean b2 = s1.startsWith("ll" ); System.out.println(b1); System.out.println(b2); boolean b3 = s1.contains("llo" ); System.out.println(b3); int i1 = s1.indexOf("lo" ); int i2 = s1.indexOf("m" ); System.out.println(i1); System.out.println(i2); }
代码示例4
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Test public void test4 () { String s1 = "helloworld" ; String s2 = s1.replace("h" , "a" ); System.out.println(s1); System.out.println(s2); String s3 = s1.replace("hello" , "hongyi" ); System.out.println(s3); String s4 = "12hello34world4" ; String s5 = s4.replaceAll("\\d+" , "," ).replaceAll("^,|,$" , "" ); System.out.println(s5); }
代码示例5
1 2 3 4 5 6 7 8 9 10 11 @Test public void test5 () { String s1 = "hello|world|java" ; String[] s2 = s1.split("\\|" ); for (String s : s2) { System.out.println(s); } }
2.1.5 数据转换 ① 基本数据类型
字符串 –> 基本数据类型、包装类
Integer包装类的public static int parseInt(String s)
:可以将由“数字”字符组成的字符串 转换为整型。
类似地,使用java.lang包中的Byte、Short、Long、Float、Double类调相应的类方法可以将由“数字”字符组成的字符串,转化为相应的基本数据类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test public void test0 () { String s1 = "123" ; int num = Integer.parseInt(s1); System.out.println(num); String s2 = "true" ; boolean flag = Boolean.parseBoolean(s2); System.out.println(s2); String s3 = "123.123" ; double num1 = Double.parseDouble(s3); System.out.println(num1); }
基本数据类型、包装类 –> 字符串
调用String类的public String valueOf(int n)
可将int型转换为字符串
相应的重载方法:valueOf(byte b)、valueOf(long l)、valueOf(float f)、valueOf(double d)、valueOf(boolean b)可由参数的相应类型到字符串的转换
1 2 3 4 5 6 @Test public void test1 () { int num = 123 ; String s = String.valueOf(num); System.out.println(s); }
② char数组
字符数组 –> 字符串
String 类的构造器:String(char[]) 和 String(char[], int offset, int length)
分别用字符数组中的全部字符和部分字符创建字符串对象。
1 2 3 4 5 6 7 @Test public void test3 () { char [] arr = new char []{'h' , 'e' , 'l' , 'l' , 'o' }; String s = new String (arr); System.out.println(s); }
字符串 –>字符数组
public char[] toCharArray()
:将字符串中的全部字符存放在一个字符数组中的方法。
public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)
:提供了将指定索引范围内的字符串存放到数组中的方法。
1 2 3 4 5 6 7 8 @Test public void test2 () { String s1 = "123abc" ; char [] charArray = s1.toCharArray(); for (char c : charArray){ System.out.println(c); } }
③ byte数组
字节数组 –> 字符串
String(byte[])
:通过使用平台的默认字符集解码指定的 byte 数组,构造一个新的 String。
String(byte[], int offset, int length)
:用指定的字节数组的一部分,即从数组起始位置offset开始取length个字节构造一个字符串对象。
1 2 3 4 5 6 7 8 @Test public void test5 () { String s1 = "abc123" ; byte [] bytes = s1.getBytes(); String s = new String (bytes); System.out.println(s); }
字符串 –> 字节数组
public byte[] getBytes()
:使用平台的默认字符集将此 String 编码为byte 序列,并将结果存储到一个新的 byte 数组中。
public byte[] getBytes(String charsetName)
:使用指定的字符集将 此 String 编码到 byte 序列,并将结果存储到新的 byte 数组。
1 2 3 4 5 6 7 8 9 @Test public void test4 () { String s1 = "abc123" ; byte [] bytes = s1.getBytes(); for (byte b : bytes) { System.out.println(b); } }
2.1.6 StringBuffer和StringBuilder类 ① 介绍 java.lang.StringBuffer
代表可变的字符序列 ,JDK1.0中声明,可以对字符串内容进行增删,此时不会产生新的对象。很多方法与String相同。作为参数传递时,方法内部可以改变值。
StringBuilder 和 StringBuffer 非常类似,均代表可变的字符序列 ,而且提供相关功能的方法也一样
② StringBuffer类
StringBuffer源码
源码分析
1 2 String s1 = new String (); String s2 = new String ("abc" );
常用方法
StringBuffer append(xxx)
:提供了很多重载的append()方法,用于进行字符串拼接
StringBuffer delete(int start,int end)
:删除指定位置的内容,左闭右开
StringBuffer replace(int start, int end, String str)
:把[start,end)位置替换为str
StringBuffer insert(int offset, xxx)
:在指定位置插入xxx
StringBuffer reverse()
:把当前字符序列逆转
public int indexOf(String str)
public String substring(int start,int end)
public int length()
public char charAt(int n )
public void setCharAt(int n ,char ch)
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Test public void test1 () { StringBuffer s1 = new StringBuffer ("abc" ); s1.append('1' ); s1.append(1 ); System.out.println(s1); s1.delete(0 , 1 ); System.out.println(s1); s1.replace(0 , 1 , "ab" ); System.out.println(s1); System.out.println(s1.reverse()); }
以上方法支持方法链
1 2 3 StringBuffer s1 = new StringBuffer ("abc" );s1.append("d" ).append("e" ).reverse(); System.out.println(s1);
③ StringBuilder类 StringBuilder和StringBuffer底层实现基本一致,方法与StringBuffer也一致,故略。
三者效率对比
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @Test public void test2 () { long startTime = 0L ; long endTime = 0L ; String text = "" ; StringBuffer buffer = new StringBuffer ("" ); StringBuilder builder = new StringBuilder ("" ); startTime = System.currentTimeMillis(); for (int i = 0 ; i < 20000 ; i++) { buffer.append(String.valueOf(i)); } endTime = System.currentTimeMillis(); System.out.println("StringBuffer的执行时间:" + (endTime - startTime)); startTime = System.currentTimeMillis(); for (int i = 0 ; i < 20000 ; i++) { builder.append(String.valueOf(i)); } endTime = System.currentTimeMillis(); System.out.println("StringBuilder的执行时间:" + (endTime - startTime)); startTime = System.currentTimeMillis(); for (int i = 0 ; i < 20000 ; i++) { text = text + i; } endTime = System.currentTimeMillis(); System.out.println("String的执行时间:" + (endTime - startTime)); }
1 2 3 StringBuffer的执行时间:4 StringBuilder的执行时间:2 String的执行时间:188
例题
1 2 3 4 5 6 7 8 9 10 11 12 @Test public void test3 () { String str = null ; StringBuffer sb = new StringBuffer (); sb.append(str); System.out.println(sb.length()); System.out.println(sb); StringBuffer sb1 = new StringBuffer (str); System.out.println(sb1); }
解析:
append()
方法在添加null
时,调用appendNull()
:该方法会把null看作是字符串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private AbstractStringBuilder appendNull () { ensureCapacityInternal(count + 4 ); int count = this .count; byte [] val = this .value; if (isLatin1()) { val[count++] = 'n' ; val[count++] = 'u' ; val[count++] = 'l' ; val[count++] = 'l' ; } else { count = StringUTF16.putCharsAt(val, count, 'n' , 'u' , 'l' , 'l' ); } this .count = count; return this ; }
1 2 3 4 5 public StringBuffer (String str) { super (str.length() + 16 ); append(str); }
2.2 日期时间相关的类 2.2.1 JDK8之前的API ① System类 System类提供的public static long currentTimeMillis()
用来返回当前时间与1970年1月1日0时0分0秒之间以毫秒 为单位的时间差。此方法适于计算时间差。
计算世界时间的主要标准有:
UTC(Coordinated Universal Time)
GMT(Greenwich Mean Time)
CST(Central Standard Time)
代码演示
1 2 3 4 5 @Test public void test0 () { long time = System.currentTimeMillis(); System.out.println(time); }
② Date类 表示特定的瞬间,精确到毫秒 。
构造器
Date()
:使用无参构造器创建的对象可以获取本地当前时间 。
Date(long date)
:其他参数的构造器已被弃用
常用方法
getTime()
:返回自 1970 年 1 月 1 日 00:00:00 GMT 以来此 Date 对象表示的毫秒数 。
toString()
:把此 Date 对象转换为以下形式的 String:dow mon dd hh:mm:ss zzz yyyy
其中:dow 是一周中的某一天 (Sun, Mon, Tue, Wed, Thu, Fri, Sat),zzz是时间标准。
其它很多方法都过时了。
代码演示
1 2 3 4 5 6 7 8 9 10 11 12 @Test public void test1 () { Date date1 = new Date (); System.out.println(date1); System.out.println(date1.getTime()); Date date2 = new Date (1648282572968L ); System.out.println(date2); }
注意
java.sql.Date
继承了java.util.Date
,对应着数据库中的日期类型的变量。
1 2 3 4 5 @Test public void test1 () { java.sql.Date date = new java .sql.Date(1648282572968L ); System.out.println(date); }
java.util.Date
–> java.util.Date
1 2 3 4 5 6 7 8 9 10 @Test public void test1 () { Date date1 = new java .sql.Date(); java.sql.Date date2 = (java.sql.Date) date1; Date date3 = new Date (); java.sql.Date date4 = new java .sql.Date(date3.getTime()); }
Date类的API不易于国际化,大部分被废弃了,java.text.SimpleDateFormat
类是一个不与语言环境有关的方式来格式化和解析日期的具体类。
它允许进行格式化:日期 –> 文本、解析:文本 –> 日期
格式化
SimpleDateFormat()
:默认的模式和语言环境创建对象
public SimpleDateFormat(String pattern)
:该构造方法可以用参数pattern指定的格式创建一个对象,该对象调用。其中pattern格式:
public String format(Date date)
:方法格式化时间对象date
解析
public Date parse(String source)
:从给定字符串的开始解析文本,以生成一个日期。
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Test public void test2 () throws ParseException { SimpleDateFormat sdf = new SimpleDateFormat (); Date date = new Date (); String format = sdf.format(date); System.out.println(format); String str = "22/3/28 上午11:43" ; Date date1 = sdf.parse(str); System.out.println(date1); SimpleDateFormat sdf1 = new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss" ); String format1 = sdf1.format(date); System.out.println(format1); Date date2 = sdf1.parse("2020-01-01 00:00:00" ); System.out.println(date2); }
④ Calendar类 略
2.2.2 JDK8中新的日期时间API ① 新API的引入 如果我们可以跟别人说:“我们在1502643933071见面,别晚了!”那么就再简单不过了。但是我们希望时间与昼夜和四季有关,于是事情就变复杂了。JDK 1.0中包含了一个java.util.Date类,但是它的大多数方法已经在JDK 1.1引入Calendar类之后被弃用了。而Calendar并不比Date好多少。它们面临的问题是:
可变性:像日期和时间这样的类应该是不可变的。
偏移性:Date中的年份是从1900开始的,而月份都从0开始。
格式化:格式化只对Date有用,Calendar则不行。
此外,它们也不是线程安全的;不能处理闰秒等。
第三次引入的API是成功的,并且Java 8中引入的java.time
API 已经纠正了过去的缺陷,将来很长一段时间内它都会为我们服务。
新日期时间API
java.time
– 包含值对象的基础包
java.time.chrono
– 提供对不同的日历系统的访问
java.time.format
– 格式化和解析时间和日期
java.time.temporal
– 包括底层框架和扩展特性
java.time.zone
– 包含时区支持的类
说明:大多数开发者只会用到基础包和format包 ,也可能会用到temporal包。因此,尽管有68个新的公开类型,大多数开发者,大概将只会用到其中的三分之一。
② LocalDate类 LocalDate
、LocalTime
、LocalDateTime
类是其中较重要的几个类,它们的实例是不可变的对象 ,分别表示使用 ISO-8601日历系统的日期、时间、日期和时间。它们提供了简单的本地日期或时间,并不包含当前的时间信息,也不包含与时区相关的信息。
LocalDate
代表IOS格式(yyyy-MM-dd)的日期,可以存储 生日、纪念日等日期。
LocalTime
表示一个时间,而不是日期。
LocalDateTime
是用来表示日期和时间的,这是一个最常用的类之一。
常用方法
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Test public void test3 () { LocalDate localDate = LocalDate.now(); LocalTime localTime = LocalTime.now(); LocalDateTime localDateTime = LocalDateTime.now(); System.out.println(localDate); System.out.println(localTime); System.out.println(localDateTime); LocalDateTime localDateTime1 = LocalDateTime.of(2020 , 10 ,6 , 13 , 23 , 59 ); System.out.println(localDateTime1); System.out.println(localDateTime.getDayOfMonth()); System.out.println(localDateTime.getDayOfWeek()); }
③ Instant类 Instant:时间线上的一个瞬时点。 这可能被用来记录应用程序中的事件时间戳。
在处理时间和日期的时候,我们通常会想到年,月,日,时,分,秒。然而,这只是时间的一个模型,是面向人类的。第二种通用模型是面向机器的,或者说是连续的。在此模型中,时间线中的一个点表示为一个很大的数,这有利于计算机处理。在UNIX中,这个数从1970年开始,以秒为的单位;同样的,在Java中,也是从1970年开始,但以毫秒 为单位。
java.time
包通过值类型Instant提供机器视图,不提供处理人类意义上的时间单位。Instant表示时间线上的一点,而不需要任何上下文信息 ,例如,时区。概念上讲,它只是简单的表示自1970年1月1日0时0分0秒(UTC)开始的秒数。因为java.time包是基于纳秒计算的,所以Instant的精度可以达到纳秒级 。
常用方法
时间戳 是指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数 。
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Test public void test4 () { Instant instant = Instant.now(); System.out.println(instant); OffsetDateTime offsetDateTime = instant.atOffset(ZoneOffset.ofHours(8 )); System.out.println(offsetDateTime); long milli = instant.toEpochMilli(); System.out.println(milli); Instant instant1 = Instant.ofEpochMilli(1648450257945L ); System.out.println(instant1); }
java.time.format.DateTimeFormatter
类:该类提供了三种格式化方法:
预定义的标准格式。如:ISO_LOCAL_DATE_TIME;ISO_LOCAL_DATE;ISO_LOCAL_TIME
本地化相关的格式。如:ofLocalizedDateTime(FormatStyle.LONG)
自定义的格式。如:ofPattern("yyyy-MM-dd hh:mm:ss")
常用方法
ofPattern(String pattern)
静态方法 , 返 回 一 个 指 定 字 符 串 格 式 DateTimeFormatter
format(TemporalAccessor t)
格式化一个日期、时间,返回字符串
parse(CharSequence text)
将指定格式的字符序列解析为一个日期、时间
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Test public void test5 () { DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME; LocalDateTime localDateTime = LocalDateTime.now(); String str1 = formatter.format(localDateTime); System.out.println("格式化之前: " + localDateTime); System.out.println("格式化之后: " + str1); TemporalAccessor parse = formatter.parse("2022-03-28T14:58:27.8932635" ); System.out.println(parse); DateTimeFormatter formatter1 = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT); String str2 = formatter1.format(localDateTime); System.out.println(str2); DateTimeFormatter formatter2 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss" ); String str3 = formatter2.format(localDateTime); System.out.println(str3); TemporalAccessor parse1 = formatter2.parse("2021-01-01 13:00:01" ); System.out.println(parse1); }
⑤ 其他API 略
2.3 比较器 在Java中经常会涉及到对象数组的排序问题,那么就涉及到对象之间的比较问题。
Java实现对象排序的方式有两种:
自然排序:java.lang.Comparable
定制排序:java.util.Comparator
2.3.1 自然排序 ① 概述 Comparable
接口强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序。
实现 Comparable 的类必须实现 compareTo(Object obj)
方法,两个对象即通过compareTo(Object obj) 方法的返回值来比较大小。如果当前对象this大于形参对象obj,则返回正整数,如果当前对象this小于形参对象obj,则返回负整数,如果当前对象this等于形参对象obj,则返回零。
实现Comparable接口的对象列表(和数组)可以通过 Collections.sort
或Arrays.sort
进行自动排序 。实现此接口的对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器。
Comparable 的典型实现:**(默认都是从小到大排列的)**
String:按照字符串中字符的Unicode值进行比较
Character:按照字符的Unicode值来进行比较
数值类型对应的包装类以及BigInteger、BigDecimal:按照它们对应的数值大小进行比较
Boolean:true 对应的包装类实例大于 false 对应的包装类实例
Date、Time等:后面的日期时间比前面的日期时间大
② 实例及使用
像String,包装类等实现了Comparable
接口,重写了compareTo()
方法,给出了比较两个对象大小的方式。
重写compareTo()
的规则:
如果当前对象this大于形参对象obj,则返回正整数
如果当前对象this小于形参对象obj,则返回负整数
如果当前对象this等于形参对象obj,则返回零 。
对于自定义类来说,如果需要排序,可以让自定义类重写Comparable接口,重写compareTo方法来指明如何排序。
String重写的compareTo()
1 2 3 4 5 6 7 8 9 10 public int compareTo (String anotherString) { byte v1[] = value; byte v2[] = anotherString.value; if (coder() == anotherString.coder()) { return isLatin1() ? StringLatin1.compareTo(v1, v2) : StringUTF16.compareTo(v1, v2); } return isLatin1() ? StringLatin1.compareToUTF16(v1, v2) : StringUTF16.compareToLatin1(v1, v2); }
代码示例——String实现自然排序
1 2 3 4 5 6 7 @Test public void test0 () { String[] arr = new String []{"AA" , "CC" , "KK" , "MM" , "GG" , "JJ" , "DD" }; Arrays.sort(arr); System.out.println(Arrays.toString(arr)); }
代码示例——自定义类实现自然排序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Data @AllArgsConstructor @ToString public class Goods implements Comparable { private String name; private double price; @Override public int compareTo (Object o) { if (o instanceof Goods){ Goods goods = (Goods) o; if (this .price > goods.price){ return 1 ; }else if (this .price < goods.price){ return -1 ; }else { return -this .name.compareTo(goods.name); } } throw new RuntimeException ("传入的数据类型不一致" ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Test public void test1 () { Goods[] goods = new Goods [5 ]; goods[0 ] = new Goods ("lenovo" , 34 ); goods[1 ] = new Goods ("dell" , 43 ); goods[2 ] = new Goods ("xiaomi" , 12 ); goods[3 ] = new Goods ("huawei" , 65 ); goods[4 ] = new Goods ("microsoft" , 43 ); Arrays.sort(goods); System.out.println(Arrays.toString(goods)); }
2.3.2 定制排序 ① 概述 当元素的类型没有实现java.lang.Comparable接口而又不方便修改代码,或者实现了java.lang.Comparable
接口的排序规则不适合当前的操作,那么可以考虑使用 Comparator
的对象来排序,强行对多个对象进行整体排序的比较。
重写compare(Object o1,Object o2)
方法:
比较o1和o2的大小:如果方法返回正整数,则表示o1大于o2
如果返回0,表示相等
返回负整数,表示o1小于o2。
可以将 Comparator 传递给 sort 方法 (如 Collections.sort
或 Arrays.sort
),从而允许在排序顺序上实现精确控制。
还可以使用 Comparator 来控制某些数据结构(如有序 set或有序映射)的顺序,或者为那些没有自然顺序的对象 collection 提供排序。
② 实例及使用
代码示例——String类实现Comparator接口
1 2 3 4 5 6 7 8 9 10 11 12 13 @Test public void test2 () { String[] arr = new String []{"AA" , "CC" , "KK" , "MM" , "GG" , "JJ" , "DD" }; Arrays.sort(arr, new Comparator <String>() { @Override public int compare (String o1, String o2) { return -o1.compareTo(o2); } }); System.out.println(Arrays.toString(arr)); }
代码示例——自定义类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @Test public void test3 () { Goods[] goods = new Goods [6 ]; goods[0 ] = new Goods ("lenovo" , 34 ); goods[1 ] = new Goods ("dell" , 43 ); goods[2 ] = new Goods ("xiaomi" , 12 ); goods[3 ] = new Goods ("huawei" , 65 ); goods[4 ] = new Goods ("microsoft" , 43 ); goods[5 ] = new Goods ("microsoft" , 12 ); Arrays.sort(goods, new Comparator <Goods>() { @Override public int compare (Goods o1, Goods o2) { if (o1.getName().equals(o2.getName())){ return -Double.compare(o1.getPrice(), o2.getPrice()); }else { return o1.getName().compareTo(o2.getName()); } } }); System.out.println(Arrays.toString(goods)); }
③ 二者对比
Comparable
一旦指定,能够保证实现类的对象能在任何位置都可以比较大小
Comparator
属于临时性的比较
2.4 System类 System类代表系统,系统级的很多属性和控制方法都放置在该类的内部。该类位于java.lang包。
由于该类的构造器是private的,所以无法创建该类的对象,也就是无法实例化该类。其内部的成员变量和成员方法都是static的,所以也可以很方便的进行调用。
成员变量
System类内部包含in
、out
和err
三个成员变量 ,分别代表标准输入流(键盘输入),标准输出流(显示器)和标准错误输出流(显示器)。
成员方法
native long currentTimeMillis()
: 该方法的作用是返回当前的计算机时间,时间的表达格式为当前计算机时间和GMT时间(格林威治时间)1970年1月1号0时0分0秒所差的毫秒数。
void exit(int status)
: 该方法的作用是退出程序。其中status的值为0代表正常退出,非零代表异常退出。使用该方法可以在图形界面编程中实现程序的退出功能等。
void gc()
: 该方法的作用是请求系统进行垃圾回收。至于系统是否立刻回收,则取决于系统中垃圾回收算法的实现以及系统执行时的情况。
String getProperty(String key)
: 该方法的作用是获得系统中属性名为key的属性对应的值。系统中常见的属性名以及属性的作用如下表所示:
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Test public void test0 () { String javaVersion = System.getProperty("java.version" ); System.out.println("java的version:" + javaVersion); String javaHome = System.getProperty("java.home" ); System.out.println("java的home:" + javaHome); String osName = System.getProperty("os.name" ); System.out.println("os的name:" + osName); String osVersion = System.getProperty("os.version" ); System.out.println("os的version:" + osVersion); String userName = System.getProperty("user.name" ); System.out.println("user的name:" + userName); String userHome = System.getProperty("user.home" ); System.out.println("user的home:" + userHome); String userDir = System.getProperty("user.dir" ); System.out.println("user的dir:" + userDir); }
3 枚举类和注解 3.1 枚举类的使用 3.1.1 概述 类的对象只有有限个,确定的。举例如下:
星期:Monday(星期一)、……、Sunday(星期天)
性别:Man(男)、Woman(女)
季节:Spring(春节)……Winter(冬天)
支付方式:Cash(现金)、WeChatPay(微信)、Alipay(支付宝)、BankCard(银 行卡)、CreditCard(信用卡)
就职状态:Busy、Free、Vocation、Dimission
订单状态:Nonpayment(未付款)、Paid(已付款)、Delivered(已发货)、Return(退货)、Checked(已确认)Fulfilled(已配货)
线程状态:创建、就绪、运行、阻塞、死亡
当需要定义一组常量时,强烈建议使用枚举类。
枚举类的实现
若枚举只有一个对象, 则可以作为一种单例模式的实现方式。
枚举类的属性
枚举类对象的属性不应允许被改动,所以应该使用 private final
修饰
枚举类的使用 private final
修饰的属性应该在构造器中为其赋值
若枚举类显式的定义了带参数的构造器,则在列出枚举值时也必须对应的传入参数
3.1.2 自定义枚举类 JDK1.5之前需要自定义枚举类
私有化类的构造器,保证不能在类的外部创建其对象
在类的内部创建枚举类的实例,声明为:public static final
对象如果有实例变量,应该声明为private final
,并在构造器中初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 public class EnumTest { @Test public void test0 () { System.out.println(Season.AUTUMN); System.out.println(Season.SPRING); } } class Season { private final String seasonName; private final String seasonDesc; private Season (String seasonName, String seasonDesc) { this .seasonName = seasonName; this .seasonDesc = seasonDesc; } public static final Season SPRING = new Season ("春天" , "春暖花开" ); public static final Season SUMMER = new Season ("夏天" , "烈日当空" ); public static final Season AUTUMN = new Season ("秋天" , "秋高气爽" ); public static final Season WINTER = new Season ("冬天" , "冰天雪地" ); public String getSeasonName () { return seasonName; } public String getSeasonDesc () { return seasonDesc; } @Override public String toString () { return "Season{" + "seasonName='" + seasonName + '\'' + ", seasonDesc='" + seasonDesc + '\'' + '}' ; } }
3.1.3 使用Enum定义枚举类 ① 使用说明
使用 enum
定义的枚举类默认继承了 java.lang.Enum
类,因此不能再继承其他类
枚举类的构造器只能使用 private
权限修饰符
枚举类的所有实例必须在枚举类中显式列出(,
分隔 ;
结尾)。对于列出的实例,系统会自动 添加 public static final
修饰
必须在枚举类的第一行 声明枚举类对象
JDK 1.5 中可以在 switch 表达式中使用Enum定义的枚举类的对象作为表达式,case 子句可以直接使用枚举值的名字,无需添加枚举类作为限定。
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public class EnumTest { @Test public void test0 () { System.out.println(Season.SPRING); System.out.println(Season.class.getSuperclass()); } } enum Season { SPRING("春天" , "春暖花开" ), SUMMER("夏天" , "烈日当空" ), AUTUMN("秋天" , "秋高气爽" ), WINTER("冬天" , "冰天雪地" ); private final String seasonName; private final String seasonDesc; private Season (String seasonName, String seasonDesc) { this .seasonName = seasonName; this .seasonDesc = seasonDesc; } public String getSeasonName () { return seasonName; } public String getSeasonDesc () { return seasonDesc; } }
② Enum类的主要方法
values()
:返回枚举类型的对象数组。该方法可以很方便地遍历所有的枚举值。
valueOf(String str)
:可以把一个字符串转为对应的枚举类对象。要求字符串必须是枚举类对象的“名字”。如不是,会有运行时异常:IllegalArgumentException
toString()
:返回当前枚举类对象常量的名称
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Test public void test1 () { Season summer = Season.SUMMER; System.out.println(summer.toString()); Season[] seasons = Season.values(); for (Season season:seasons){ System.out.println(season); } Thread.State[] states = Thread.State.values(); for (Thread.State state:states){ System.out.println(state); } Season winter = Season.valueOf("WINTER" ); System.out.println(winter); }
③ 实现接口的枚举类
和普通 Java 类一样,枚举类可以实现一个或多个接口
若每个枚举值在调用实现的接口方法呈现相同的行为方式,则只要统一实现该方法即可。
若需要每个枚举值在调用实现的接口方法呈现出不同的行为方式,则可以让每个枚举值分别来实现该方法
代码示例
情况一:若每个枚举值在调用实现的接口方法呈现相同的行为方式,则只要统一实现该方法即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 public class EnumTest { @Test public void test2 () { Season autumn = Season.AUTUMN; autumn.show(); } } interface Info { void show () ; } enum Season implements Info { SPRING("春天" , "春暖花开" ), SUMMER("夏天" , "烈日当空" ), AUTUMN("秋天" , "秋高气爽" ), WINTER("冬天" , "冰天雪地" ); private final String seasonName; private final String seasonDesc; private Season (String seasonName, String seasonDesc) { this .seasonName = seasonName; this .seasonDesc = seasonDesc; } public String getSeasonName () { return seasonName; } public String getSeasonDesc () { return seasonDesc; } @Override public void show () { System.out.println("这是一个季节" ); } }
情况二:若需要每个枚举值在调用实现的接口方法呈现出不同的行为方式,则可以让每个枚举值分别来实现该方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 public class EnumTest { @Test public void test2 () { Season autumn = Season.AUTUMN; autumn.show(); } } interface Info { void show () ; } enum Season implements Info { SPRING("春天" , "春暖花开" ){ @Override public void show () { System.out.println("这是春天" ); } }, SUMMER("夏天" , "烈日当空" ){ @Override public void show () { System.out.println("这是夏天" ); } }, AUTUMN("秋天" , "秋高气爽" ){ @Override public void show () { System.out.println("这是秋天" ); } }, WINTER("冬天" , "冰天雪地" ){ @Override public void show () { System.out.println("这是冬天" ); } }; private final String seasonName; private final String seasonDesc; private Season (String seasonName, String seasonDesc) { this .seasonName = seasonName; this .seasonDesc = seasonDesc; } public String getSeasonName () { return seasonName; } public String getSeasonDesc () { return seasonDesc; } }
3.2 注解的使用 3.2.1 概述 从 JDK 5.0
开始,Java 增加了对元数据(MetaData) 的支持,也就是Annotation(注解) 。
Annotation 其实就是代码里的特殊标记,这些标记可以在编译,类加载,运行时被读取,并执行相应的处理。通过使用 Annotation,程序员可以在不改变原有逻辑的情况下,在源文件中嵌入一些补充信息。代码分析工具、开发工具和部署工具可以通过这些补充信息进行验证或者进行部署。
Annotation 可以像修饰符一样被使用,可用于修饰包、类、构造器、方法,、成员变量、参数、局部变量的声明,这些信息被保存在 Annotation 的 name=value
对中。
在JavaSE中,注解的使用目的比较简单,例如标记过时的功能,忽略警告等。在JavaEE/Android中注解占据了更重要的角色,例如用来配置应用程序的任何切面,代替JavaEE旧版中所遗留的繁冗代码和XML配置等。
未来的开发模式都是基于注解的,JPA是基于注解的,Spring2.5以上都是基于注解的,Hibernate3.x以后也是基于注解的,现在的Struts2有一部分也是基于注解的了,注解是一种趋势,一定程度上可以说:==框架 = 注解 + 反射 + 设计模式==。
3.2.2 常见的Annotation示例 使用 Annotation 时要在其前面增加 @
符号,,并把该 Annotation 当成一个修饰符使用,用于修饰它支持的程序元素。
示例1——生成文档相关的注解
@author
标明开发该类模块的作者,多个作者之间使用,分割
@version
标明该类模块的版本
@see
参考转向,也就是相关主题
@since
从哪个版本开始增加的
@param
对方法中某参数的说明,如果没有参数就不能写
@return
对方法返回值的说明,如果方法的返回值类型是void就不能写
@exception
对方法可能抛出的异常进行说明,如果方法没有用throws显式抛出的异常就不能写
示例2——在编译时进行格式检查(JDK内置的三个基本注解)
@Override
: 限定重写父类方法, 该注解只能用于方法
@Deprecated
: 用于表示所修饰的元素(类,方法等)已过时。通常是因为所修饰的结构危险或存在更好的选择。
@SuppressWarnings
: 抑制编译器警告
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 public class AnnotationTest { @Test public void test0 () { Person p = new Person ("Hongyi" , 24 ); p.eat(); @SuppressWarnings("unused") int a = 10 ; } } class Person { private String name; private int age; public Person () { } public Person (String name, int age) { this .name = name; this .age = age; } public void walk () { System.out.println("walk..." ); } @Deprecated public void eat () { System.out.println("eat..." ); } } interface Infor { void show () ; } class Student extends Person implements Infor { @Override public void walk () { System.out.println("student walks..." ); } @Override public void show () { System.out.println("student shows..." ); } }
示例3——跟踪代码依赖性,实现替代配置文件功能
Servlet3.0提供了注解(annotation),使得不再需要在web.xml文件中进行Servlet的部署。
spring框架中关于“事务”的管理
3.2.3 自定义注解 ① 基本使用
定义新的 Annotation 类型使用 @interface
关键字
自定义注解自动继承了java.lang.annotation.Annotation
接口
Annotation 的成员变量在 Annotation 定义中以无参数方法 的形式来声明。其方法名和返回值定义了该成员的名字和类型。我们称为配置参数 。类型只能是八种基本数据类型、String类型、Class类型、enum类型、Annotation类型、以上所有类型的数组。
可以在定义 Annotation 的成员变量时为其指定初始值,指定成员变量的初始值可使用 default
关键字
如果只有一个参数成员,建议使用参数名为value
如果定义的注解含有配置参数,那么使用时必须指定参数值,除非它有默认值。格式是“参数名 = 参数值”,如果只有一个参数成员,且名称为value,可以省略“value=”
没有成员定义的 Annotation 称为标记 (例如@Override
),包含成员变量的 Annotation 称为元数据 Annotation
注意:自定义注解必须配上注解的信息处理流程(使用反射)才有意义。
代码示例
1 2 3 public @interface MyAnnotation { String value () default "world" ; }
1 2 3 4 @MyAnnotation(value = "hello") class Person { }
② 基本元注解 JDK 的元 Annotation 用于修饰其他 Annotation 定义,对现有的注解进行说明的注解。
JDK5.0提供了4个标准的meta-annotation类型,分别是:
Retention
Target
Documented
Inherited
@Retention
只能用于修饰一个 Annotation 定义,用于指定该 Annotation 的生命周期 ,@Rentention 包含一个 RetentionPolicy
类型的成员变量,使用@Rentention 时必须为该 value 成员变量指定值
RetentionPolicy.SOURCE
:在源文件中有效(即源文件保留),编译器直接丢弃这种策略的注释。
RetentionPolicy.CLASS
:在class文件中有效(即class保留),当运行 Java 程序时,JVM 不会保留注解。 这是默认值 。
RetentionPolicy.RUNTIME
:在运行时有效(即运行时保留),当运行 Java 程序时,JVM 会保留注释。程序可以通过反射 获取该注释。
1 2 3 4 5 @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, MODULE}) @Retention(RetentionPolicy.SOURCE) public @interface SuppressWarnings { }
@Target
用于修饰 Annotation 定义,用于指定被修饰的 Annotation 能用于修饰哪些程序元素 。 @Target 也包含一个名为 value 的成员变量。
取值(ElementType)
说明
CONSTRUCTOR
构造器
FIELD
域
LOCAL_VIRIABLE
局部变量
METHOD
方法
PACKAGE
包
PARAMETER
参数
TYPE
类、接口、enum
@Documented
用于指定被该元 Annotation 修饰的 Annotation 类将被 javadoc 工具提取成文档。默认情况下,javadoc是不包括注解的。
定义为Documented的注解必须设置Retention值为RUNTIME
@Inherited
被它修饰的 Annotation 将具有继承性。如果某个类使用了被@Inherited 修饰的 Annotation,则其子类将自动具有该注解。使用很少。
4 集合 4.1 集合框架概述 集合,数组都是对多个数据进行存储的结构,简称Java容器。说明:此时的存储是内存层面的存储,不涉及持久化的存储。
java集合可分为Collection和Map两种体系。
Collection接口:单列数据,定义了存取一组对象的方法和集合。
List:元素有序,可重复的集合
Set:元素无序,不可重复的集合
Map接口:双列数据,保存具有映射关系Key-Value对的集合。
4.1.1 数组的特点
数组在存储多个数据方面的特点
一旦初始化以后,其长度就确定了
数组一旦定义好,其元素的类型也就确定了,例如String[] arr;Object[] arr等
数组在存储多个数据方面的缺点
一旦初始化后,其长度就无法修改
数组中提供的方法非常有限,对于添加,删除,插入数据等操作非常不便,同时效率不高。
对于获取数组中实际元素的个数的需求,数组没有现成的属性或方法可用
数组存储数据的特点:有序,可重复。对于无序,不可重复的需求,数组不能满足。
4.1.2 集合框架 1 2 3 4 5 6 7 8 |----Collection接口:单列集合,用来存储一个一个的对象 |----List接口:存储有序的,可重复的数据。 |----ArrayList,LinkedList,Vector |----Set接口:存储无序的,不可重复的数据。 |----HashSet,LinkedHashSet,TreeSet |----Map接口:双列集合,用来存储一对一对的数据。 |----HashMap,LinkedHashMap,TreeMap,Hashtable,Properties
4.2 Collection接口 4.2.1 Collection接口中的常用方法
add()
,addAll()
,clear()
,size()
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Test public void test1 () { Collection coll = new ArrayList (); coll.add("AAA" ); coll.add(123 ); coll.add(new Date ()); System.out.println(coll.size()); Collection coll1 = new ArrayList (); coll1.add("BBB" ); coll1.add(new Date ()); coll.addAll(coll1); System.out.println(coll.size()); System.out.println(coll.isEmpty()); }
contains()
,containsAll()
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Test public void test2 () { Collection coll = new ArrayList (); coll.add("AAA" ); coll.add(123 ); coll.add(456 ); coll.add(new Date ()); coll.add(new String ("Tom" )); coll.add(false ); coll.add(new Person ("Jerry" ,20 )); boolean contains = coll.contains(123 ); System.out.println(contains); System.out.println(coll.contains(new String ("Tom" ))); System.out.println(coll.contains(new Person ("Jerry" ,20 ))); Collection coll1 = Arrays.asList(123 ,456 ); System.out.println(coll.containsAll(coll1)); }
Person
类中重写的equals
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Override public boolean equals (Object o) { if (this == o) return true ; if (o == null || getClass() != o.getClass()) return false ; Person person = (Person) o; if (age != person.age) return false ; return name != null ? name.equals(person.name) : person.name == null ; }
remove()
,removeAll()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Test public void test3 () { Collection coll = new ArrayList (); coll.add(123 ); coll.add(456 ); coll.add(new String ("Tom" )); coll.add(false ); coll.add(new Person ("Jerry" ,20 )); System.out.println(coll.remove(123 )); System.out.println(coll.remove(new Person ("Jerry" , 20 ))); Collection coll1 = Arrays.asList(123 ,456 ); coll.removeAll(coll1); System.out.println(coll); }
retainAll()
equals()
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Test public void test4 () { Collection coll = new ArrayList (); coll.add(123 ); coll.add(456 ); coll.add(new String ("Tom" )); coll.add(false ); coll.add(new Person ("Jerry" ,20 )); Collection coll1 = new ArrayList (); coll1.add(123 ); coll1.add(456 ); coll1.add(new String ("Tom" )); coll1.add(false ); coll1.add(new Person ("Jerry" ,20 )); System.out.println(coll.equals(coll1)); }
hashCode()
,toArray()
,Arrays.asList()
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Test public void test5 () { Collection coll = new ArrayList (); coll.add(123 ); coll.add(456 ); coll.add(new String ("Tom" )); coll.add(false ); coll.add(new Person ("Jerry" ,20 )); System.out.println(coll.hashCode()); Object[] array = coll.toArray(); for (int i=0 ;i<array.length;i++){ System.out.println(array[i]); } List<String> list = Arrays.asList(new String []{"AAA" , "BBB" , "CCC" }); System.out.println(list); }
4.2.2 集合元素的遍历 集合元素的遍历操作,需要使用Iterator
接口。
设计模式给迭代器模式的定义为:提供一种方法访问一个容器对象中的各个元素,而又不需暴露该对象的内部细节。迭代器模式,就是为容器而生。
Collection接口继承了java.lang.Iterable
接口,该接口有一个iterator()
方法,那么所有实现了Collection接口的集合类都有一个iterator方法,用以返回一个实现了Iterator接口的对象。
Iterator仅用于遍历集合,其本身不具有提供承载对象的能力。如果需要创建Iterator对象,则必须有一个被迭代的集合。
==集合对象每次调用iterator方法都得到一个全新的迭代器对象==,默认游标都在集合的第一个元素之前 。
遍历代码演示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @Test public void test6 () { Collection coll = new ArrayList (); coll.add(123 ); coll.add(456 ); coll.add(new String ("Tom" )); coll.add(false ); coll.add(new Person ("Jerry" ,20 )); Iterator iterator = coll.iterator(); System.out.println(iterator.next()); System.out.println(iterator.next()); System.out.println(iterator.next()); System.out.println(iterator.next()); System.out.println(iterator.next()); Iterator iterator1 = coll.iterator(); for (int i=0 ;i<coll.size();i++){ System.out.println(iterator1.next()); } Iterator iterator2 = coll.iterator(); while (iterator2.hasNext()){ System.out.println(iterator2.next()); } }
迭代器原理
用迭代器遍历的错误写法
1 2 3 4 5 6 7 8 Iterator iterator = coll.iterator();while ((iterator.next()) != null ){ System.out.println(iterator.next()); } while (coll.iterator().hasNext()){ System.out.println(iterator.next()); }
迭代器的remove()
可以在遍历的时候删除集合中的元素。此方法不同于集合直接调用remove
方法
代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Test public void test7 () { Collection coll = new ArrayList (); coll.add(123 ); coll.add(456 ); coll.add(new String ("Tom" )); coll.add(false ); coll.add(new Person ("Jerry" ,20 )); Iterator iterator = coll.iterator(); while (iterator.hasNext()){ Object obj = iterator.next(); if ("Tom" .equals(obj)){ iterator.remove(); } } iterator = coll.iterator(); while (iterator.hasNext()){ System.out.println(iterator.next()); } }
注意:如果还未调用next就调用remove,或者调用一次remove后再次调用remove,会报IllegalStateException
异常。
4.2.3 foreach
循环遍历 jdk5.0
新增特性,用于遍历数组和集合。
代码演示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Test public void test () { Collection coll = new ArrayList (); coll.add(123 ); coll.add(456 ); coll.add(new String ("Tom" )); coll.add(false ); coll.add(new Person ("Jerry" ,20 )); for (Object obj : coll){ System.out.println(obj); } int arr[] = new int []{1 ,2 ,3 ,4 ,5 ,6 ,7 }; for (int i: arr) { System.out.println(i); } }
一个练习题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Test public void test1 () { String arr[] = new String []{"MM" ,"MM" ,"MM" }; for (int i=0 ;i<arr.length;i++){ arr[i] = "GG" ; } for (String s : arr){ s = "GG" ; } for (int i=0 ;i<arr.length;i++){ System.out.println(arr[i]); } }
4.3 List接口及其实现类 4.3.1 List概述
List接口是Collection的子接口,通常使用List来替代数组。
List集合中的元素有序且可重复,每个元素都有对应的索引顺序,可根据整型序号来对元素进行存取。
List接口的常用实现类有ArrayList
,LinkedList
和Vector
面试题:实现类三者的异同?
同:都是List接口的实现类,存储的都是有序可重复的数据。
异:
1 2 3 4 5 |----Collection接口:单列集合,用来存储一个一个的对象 |----List接口:存储有序的,可重复的数据。“动态数组” |----ArrayList:JDK1.2,作为List接口的主要实现类,线程不安全的,效率高;底层采用Object[] elementData存储(顺序表),相当于C++中的vector |----LinkedList:JDK1.2,底层采用的双向链表存储,对于频繁的插入和删除操作的效率比上者高。相当于C++中的list |----Vector:JDK1.0,是List接口的古老实现类,线程安全的,效率低
4.3.2 ArrayList源码分析
JDK7
版本——饿汉式
1 2 3 4 ArrayList list = new ArrayList ();list.add(123 ); list.add(123 );
结论:实际开发中使用带参的构造器,指定出大小:
1 ArrayList list = new ArrayList (int capacity);
JDK8
版本——懒汉式
1 2 3 ArrayList list = new ArrayList ();list.add(123 );
默认构造函数:
1 2 3 4 5 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};public ArrayList () { this .elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
添加一个元素:
1 2 3 4 5 6 7 8 9 private void add (E e, Object[] elementData, int s) { if (s == elementData.length) elementData = grow(); elementData[s] = e; size = s + 1 ; }
扩容函数:
1 2 3 4 5 6 7 8 private Object[] grow() { return grow(size + 1 ); } private Object[] grow(int minCapacity) { return elementData = Arrays.copyOf(elementData, newCapacity(minCapacity)); }
可以看出调用了Arrays.copyOf
复制了一份elementData
,并且设置容量为newCapacity(minCapacity)
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 private int newCapacity (int minCapacity) { int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1 ); if (newCapacity - minCapacity <= 0 ) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) return Math.max(DEFAULT_CAPACITY, minCapacity); if (minCapacity < 0 ) throw new OutOfMemoryError (); return minCapacity; } return (newCapacity - MAX_ARRAY_SIZE <= 0 ) ? newCapacity : hugeCapacity(minCapacity); }
小结
jdk7的ArrayList对象的创建类似于单例的饿汉式,jdk8类似于单例的懒汉式,延迟了数组的创建,节省内存。
4.3.3 LinkedList源码分析
JDK8
版本
1 2 3 4 5 6 7 8 9 10 11 12 private static class Node <E> { E item; LinkedList.Node<E> next; LinkedList.Node<E> prev; Node(LinkedList.Node<E> prev, E element, LinkedList.Node<E> next) { this .item = element; this .next = next; this .prev = prev; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 void linkLast (E e) { LinkedList.Node<E> l = this .last; LinkedList.Node<E> newNode = new LinkedList .Node(l, e, (LinkedList.Node)null ); this .last = newNode; if (l == null ) { this .first = newNode; } else { l.next = newNode; } ++this .size; ++this .modCount; }
1 2 3 LinkedList list = new LinkedList ();list.add(123 );
4.3.4 List接口的常用方法 List接口除了有从Collection接口继承的方法外,还添加了一些根据索引来操作集合元素的方法。
代码演示说明
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 @Test public void test () { ArrayList list = new ArrayList (); list.add(123 ); list.add(456 ); list.add(new String ("Tom" )); list.add(false ); list.add(new Person ("Jerry" ,20 )); list.add(1 ,"BB" ); System.out.println(list); List list1 = Arrays.asList("hongyi" ,true ); list.addAll(1 ,list1); System.out.println(list); System.out.println(list.get(1 )); System.out.println(list.indexOf("BB" )); System.out.println(list.remove(0 )); List list2 = list.subList(1 ,4 ); System.out.println(list2); }
总结常用方法
4.3.5 List集合的遍历
代码演示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Test public void test1 () { ArrayList list = new ArrayList (); list.add(123 ); list.add(456 ); list.add(new String ("Tom" )); list.add(false ); list.add(new Person ("Jerry" ,20 )); Iterator iterator = list.iterator(); while (iterator.hasNext()){ System.out.println(iterator.next()); } for (Object obj : list){ System.out.println(obj); } for (int i=0 ;i<list.size();i++){ System.out.println(list.get(i)); } }
一道笔试题
1 2 3 4 5 6 7 8 9 10 11 12 13 @Test public void test2 () { List list = new ArrayList (); list.add(1 ); list.add(2 ); list.add(3 ); updateList(list); System.out.println(list); } private static void updateList (List list) { list.remove(2 ); }
注意区分List中的remove
方法
4.4 Set接口及其实现类 4.4.1 Set概述 Set接口中没有额外定义新的方法,使用的都是Collection接口定义的方法
框架
1 2 3 4 5 |----Collection接口:单列集合,用来存储一个一个的对象 |----Set接口:存储无序的,不可重复的数据。 |----HashSet:作为Set的主要实现类;线程不安全,可以存储null值 |----LinkedHashSet:作为HashSet的子类;遍历其内部数据时可以按照添加的顺序去遍历 |----TreeSet:可以按照添加的对象的指定属性进行排序,底层采用红黑树
无序和无可重复的理解
无序性:不等于随机性。以HashSet为例,存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值添加。
不可重复性:保证添加的元素按照equal
方法判断时不能返回true
,即相同的元素只能添加一个。
4.4.2 HashSet添加元素的过程 向HashSet中添加元素a,首先,调用a的所在类的hashCode方法,计算出a元素的哈希值,此哈希值接着通过某种算法(例如除留余数法)计算出a在HashSet底层数组中的存放位置,即为索引位置。判断数组此位置上是否已经含有元素:
|—如果此位置上没有其他元素,则a添加成功;—>情况1
|—如果此位置上有其他元素b(或以链表形式存在的多个元素),首先比较a和b的哈希值:
|—如果哈希值不相同,则a添加成功;—>情况2
|—如果哈希值相同,则需要调用a所在类的equals方法,与链表上的元素逐一相比:
|—如果一旦返回true,则添加失败;
|—如果比较到最后返回false,则a添加成功;—>情况3
对于添加成功的情况2和情况3,元素a与已经存在指定索引位置上的数据以链表方式进行存储。在JDK7中,元素a放在数组中,指向原来的元素链;在JDK8中,原来的链尾元素指向新添加进来的元素a。(==7上8下==)
HashSet的底层为:==数组+链表==,实质上还是new了一个HashMap
4.4.3 关于equals()
和hashCode()
方法的重写
idea中自动生成的重写方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Override public boolean equals (Object o) { if (this == o) return true ; if (o == null || getClass() != o.getClass()) return false ; User user = (User) o; if (age != user.age) return false ; return name != null ? name.equals(user.name) : user.name == null ; } @Override public int hashCode () { int result = name != null ? name.hashCode() : 0 ; result = 31 * result + age; return result; }
选择31作为乘数的原因:选择系数的时候要选择尽量大的系数。因为如果计算出来的hash地址越大,所谓的“冲突”就越少,查找起来效率也会提高。(减少冲突)。并且31只占用5bits,相乘造成数据溢出的概率较小。31可以由i*31== (i<<5)-1
来表示,现在很多虚拟机里面都有做相关优化。(提高算法效率)。31是一个素数,素数作用就是如果我用一个数字来乘以这个素数,那么最终出来的结果只能被素数本身和被乘数还有1来整除!(减少冲突)
重写原则
重写hashCode方法的原则
在程序运行时,同一个对象多次调用 hashCode()
方法应该返回相同的值。
当两个对象的 equals()
方法比较返回 true 时,这两个对象的 hashCode()
方法的返回值也应相等。
对象中用作 equals()
方法比较的属性,都应该用来计算 hashCode 值。
重写equals方法的原则
当一个类有自己特有的“逻辑相等”概念,当改写equals()的时候,总是要改写hashCode(),根据一个类的equals方法(改写后),两个截然不同的实例有可能在逻辑上是相等的,但是,根据Object.hashCode()方法,它们仅仅是两个对象。
因此,违反了“相等的对象必须具有相等的散列码”。
结论:==复写equals方法的时候一般都需要同时复写hashCode方法==。通常参与计算hashCode的对象的属性也应该参与到equals()中进行计算。
4.4.4 LinkedHashSet的使用 LinkedHashSet是作为HashSet的子类,在添加数据的同时,还维护了每个数据的添加的先后顺序。即每个结点有前后两个指针域,指示上一个和下一个元素的位置。对于频繁的遍历操作,效率比HashSet高。
代码演示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Test public void test2 () { Set set = new LinkedHashSet (); set.add(123 ); set.add(456 ); set.add("AA" ); set.add("CC" ); set.add(new User ("Tom" ,23 )); set.add(new User ("Tom" ,23 )); set.add(false ); Iterator iterator = set.iterator(); while (iterator.hasNext()){ System.out.println(iterator.next()); } }
原理示意图
4.4.5 TreeSet的使用
向TreeSet中添加的数据,要求是==相同类的对象==
1 2 3 4 5 6 7 8 @Test public void test3 () { Set set = new TreeSet (); set.add(123 ); set.add(456 ); set.add("AA" ); set.add(new User ("Tom" ,23 )); }
可以按照排序后输出
1 2 3 4 5 6 7 8 9 10 11 12 @Test public void test3 () { Set set = new TreeSet (); set.add(13 ); set.add(6 ); set.add(-1 ); Iterator iterator = set.iterator(); while (iterator.hasNext()){ System.out.println(iterator.next()); } }
TreeSet的底层是==红黑树==
TreeSet的自然排序(要求比较对象所在类实现Comparable接口)
在TreeSet自然排序中,比较两个对象是否相同的标准为:compareTo()
返回值为0,不再是用equals()
判断。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test public void test3 () { Set set = new TreeSet (); set.add(new User ("Tom" ,23 )); set.add(new User ("Jerry" ,32 )); set.add(new User ("Mike" ,53 )); set.add(new User ("Mike" ,2 )); Iterator iterator = set.iterator(); while (iterator.hasNext()){ System.out.println(iterator.next()); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class User implements Comparable { private String name; private int age; public int compareTo (Object o) { if (o instanceof User){ User user = (User)o; int compare = this .name.compareTo(user.name); if (compare != 0 ){ return compare; }else { return Integer.compare(this .age,user.age); } }else { throw new RuntimeException ("输入的类型不匹配" ); } } }
TreeSet的定制排序(要求TreeSet的构造器参数为实现Comparator接口的对象)
在TreeSet定制排序中,比较两个对象是否相同的标准为:compare()
返回值为0,不再是用equals()
判断。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @Test public void test4 () { Comparator com = new Comparator () { @Override public int compare (Object o, Object t1) { if (o instanceof User && t1 instanceof User){ User u1 = (User)o; User u2 = (User)t1; return Integer.compare(u1.getAge(),u2.getAge()); }else { throw new RuntimeException ("输入的数据类型不匹配" ); } } }; Set set = new TreeSet (com); set.add(new User ("Tom" ,23 )); set.add(new User ("Jerry" ,32 )); set.add(new User ("Mike" ,53 )); set.add(new User ("Mary" ,53 )); set.add(new User ("Mike" ,2 )); Iterator iterator = set.iterator(); while (iterator.hasNext()){ System.out.println(iterator.next()); } }
4.5 Map接口及其实现类 4.5.1 Map框架概述 1 2 3 4 5 6 |----Map接口:双列集合,用来存储一对一对的数据。 |----HashMap:作为Map的主要实现类。线程不安全,效率高。可以存储null的key和value。 |----LinkedHashMap:保证在遍历Map元素时是按照添加的顺序实现遍历。在原有的HashMap的底层结构基础上,添加了一对指针,指向前一个和后一个元素。对于频繁的遍历操作,此类的执行效率要高于HashMap。 |----TreeMap:保证按照添加的键值对进行排序,实现排序遍历。此时考虑key的自然或定制排序。底层是红黑树。 |----Hashtable:注意t小写。Map的古老实现类。线程安全,效率低。不能存储null的key和value。 |----Properties:常用来处理配置文件。key和value都是String类型。
HashMap底层
jdk7
数组+链表
jdk8
数组+链表+==红黑树==
典型面试题
HashMap的底层实现原理
HashMap和Hashtable的异同
常用方法
4.5.2 Map结构的理解:key和value的特点
Map中的key:无序不可重复 ,使用Set存储所有的key;要求key所在类重写equals和hashCode方法。
Map中的value:无序可重复,使用Collection存储所有的value;要求key所在类重写equals方法。
一个键值对构成了一个Entry对象
Map中的Entry:无序不可重复,使用Set存储所有的entry
4.5.3 HashMap ① JDK7
版本 底层数据结构:Entry
数组和链表
1 HashMap map = new HashMap ();
在实例化以后,底层创建了长度是==16==的一维数组Entry[] table
首先,调用key1所在类的hashCode方法计算key1的哈希值,此哈希值经过某种算法后(例如取余),得到在Entry数组中的存放位置。
|—-如果此位置上为空,则entry对象添加成功;情况1
|—-如果此位置上不为空,意味着此位置上存在一个或多个数据(以链表形式存在),则比较key1和已经存在的数据的哈希值:
|—-如果都不相同,则添加成功,进行头插;情况2
|—-如果与某一个数据的哈希值相同,则调用key1所在类的equals方法进行比较:
|—-如果返回false,则添加成功,进行头插;情况3
|—-如果返回true,使用value1==替换==相同key的value值。
对于情况2和情况3:同HashSet一样七上八下。
扩容:默认的扩容方式为扩容为原来容量的==2倍==(即新开辟一个原来容量2倍的数组空间),并将原有数据复制到该新数组中(再散列,或者rehash
)。
哈希冲突时采用链地址法
② JDK8
版本 底层数据结构:Node
数组 + 链表 + 红黑树
1 HashMap map = new HashMap ();
底层没有创建一个长度为16的数组,并且该数组类型不是Entry了,而是Node;
首次调用put方法时,才创建数组 。(懒加载)
底层结构新增了==红黑树==。当数组的某一个索引位置上的元素以链表形式存在的==个数大于8且当前数组的长度大于64时==,此索引位置上的所有数据改为使用红黑树存储 。
4.5.4 TreeMap TreeMap
实现了SortedMap
接口,也就是说会按照key
的大小顺序对Map
中的元素进行排序,key
大小的评判可以通过其本身的自然顺序,也可以通过构造时传入的比较器。
TreeMap底层通过红黑树实现,算法复杂度为O(logN)
4.5.5 LinkedHashMap LinkedHashMap
实现了Map
接口,即允许放入key
为null
的元素,也允许插入value
为null
的元素。
和HashMap
的区别在于,在HashMap的基础上,采用双向链表的形式将所有entry
连接起来,这样是为保证元素的迭代顺序跟插入顺序相同 。
当put
一个元素时:
从table
的角度看,新的entry
需要插入到对应的bucket
里,当有哈希冲突时,采用头插法将新的entry
插入到冲突链表的头部。
从header
的角度看,新的entry
需要插入到双向链表的尾部。
4.5.6 ConcurrentHashMap 详见[[Java并发编程学习笔记#8.1 ConcurrentHashMap]]
4.6 Collections工具类 4.6.1 介绍 Collections 是一个操作 Set、List 和 Map 等集合的工具类。
Collections 中提供了一系列静态的方法 对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法。
4.6.2 常用方法 ① 排序
reverse(List)
:反转 List 中元素的顺序
shuffle(List)
:对 List 集合元素进行随机排序
sort(List)
:根据元素的自然顺序对指定 List 集合元素按升序排序
sort(List, Comparator)
:根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
swap(List, int i, int j)
:将指定 list 集合中的 i 处元素和 j 处元素进行交换
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Test public void test0 () { List<Integer> list = new ArrayList <>(); list.add(123 ); list.add(43 ); list.add(0 ); list.add(-97 ); list.add(765 ); System.out.println(list); Collections.reverse(list); System.out.println(list); Collections.sort(list); System.out.println(list); Collections.sort(list, new Comparator <Integer>() { @Override public int compare (Integer o1, Integer o2) { return -Integer.compare(o1, o2); } }); System.out.println(list); }
② 查找和替换
Object max(Collection)
:根据元素的自然顺序,返回给定集合中的最大元素
Object max(Collection, Comparator)
:根据 Comparator 指定的顺序,返回给定集合中的最大元素
Object min(Collection)
Object min(Collection, Comparator)
int frequency(Collection, Object)
:返回指定集合中指定元素的出现次数
void copy(List dest, List src)
:将src中的内容复制到dest中
boolean replaceAll(List list, Object oldVal, Object newVal)
:使用新值替换List 对象的所有旧值
代码演示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Test public void test1 () { List<Integer> list = new ArrayList <>(); list.add(123 ); list.add(43 ); list.add(0 ); list.add(-97 ); list.add(765 ); List<Integer> dest = Arrays.asList(new Integer [list.size()]); System.out.println(dest.size()); Collections.copy(dest, list); System.out.println(dest); }
③ 同步控制 Collections 类中提供了多个 synchronizedXxx()
方法,该方法可使将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题。
代码示例
1 2 3 4 5 6 7 8 9 10 11 @Test public void test2 () { List<Integer> list = new ArrayList <>(); list.add(123 ); list.add(43 ); list.add(0 ); list.add(-97 ); list.add(765 ); List<Integer> list1 = Collections.synchronizedList(list); }
5 泛型 5.1 概念 5.1.1 泛型的设计背景 集合容器类在设计阶段/声明阶段不能确定这个容器到底实际存的是什么类型的对象,所以在JDK1.5之前只能把元素类型设计为Object,JDK1.5之后使用泛型来解决。因为这个时候除了元素的类型不缺定 ,其他的部分是确定的,例如关于这个元素如何保存,如何管理等是确定的,因此此时把元素的类型设计成一个参数,这个类型参数叫做泛型。Collection<E>
,List<E>
,ArrayList<E>
这个`就是类型参数,即泛型。
5.1.2 泛型的概念
泛型,就是允许在定义类、接口时通过一个标识表示类中某个属性的类型或者是某个方法的返回值及参数类型。这个类型参数将在使用时(例如,继承或实现这个接口,用这个类型声明变量、创建对象时)确定(即传入实际的类型参数,也称为类型实参)。
从JDK1.5以后,Java引入了“参数化类型(Parameterized type)”的概念,允许我们在创建集合时再指定集合元素的类型,正如:List<String>
,这表明该List只能保存字符串类型的对象。
JDK1.5改写了集合框架中的全部接口和类,为这些接口、类增加了泛型支持,从而可以在声明集合变量、创建集合对象时传入类型实参。
5.1.3 需要泛型的理由
为什么要有泛型呢,直接Object不是也可以存储数据吗?
解决元素存储的安全性问题,好比商品、药品标签,不会弄错。
解决获取数据元素时,需要类型强制转换的问题,好比不用每回拿商品、药品都要辨别。
Java泛型可以保证如果程序在编译时没有发出警告,运行时就不会产生ClassCastException
异常。同时,代码更加简洁、健壮。
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Test public void test0 () { ArrayList list = new ArrayList (); list.add(12 ); list.add(43 ); list.add(89 ); list.add(100 ); list.add(56 ); list.add("Tom" ); for (Object score:list){ int stuScore = (int ) score; System.out.println(score); } }
5.2 集合中使用泛型 以ArrayList和HashMap为例
5.2.1 ArrayList
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @Test public void test1 () { ArrayList<Integer> list = new ArrayList <Integer>(); list.add(12 ); list.add(43 ); list.add(89 ); list.add(100 ); list.add(56 ); for (Integer score:list){ int stuScore = score; System.out.println(stuScore); } Iterator<Integer> iterator = list.iterator(); while (iterator.hasNext()){ System.out.println(iterator.next()); } }
5.2.2 HashMap
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Test public void test2 () { HashMap<String, Integer> map = new HashMap <>(); map.put("Tom" , 97 ); map.put("Jack" , 100 ); map.put("Hongyi" , 100 ); Set<Map.Entry<String, Integer>> entries = map.entrySet(); Iterator<Map.Entry<String, Integer>> iterator = entries.iterator(); while (iterator.hasNext()){ Map.Entry<String, Integer> entry = iterator.next(); String key = entry.getKey(); Integer value = entry.getValue(); System.out.println(key + "----" + value); } }
5.2.3 总结
集合接口和集合类在jdk5.0
时都修改为带泛型的结构
在实例化集合类时,可以指定具体的泛型类型
指明完以后,在集合类或接口中凡是定义类或接口时,内部结构(例如方法、构造器、属性等)使用到类的泛型的位置,都指定为实例化的泛型类型
例如add(E e)
–>实例化以后: add(Integer e)
注意:泛型的类型必须是类 ,不能是基本数据类型
1 2 ArrayList<int > list = new ArrayList <>(); ArrayList<Integer> list = new ArrayList <>();
如果实例化时没有指定泛型,默认类型为Object
类型
5.3 自定义泛型结构 5.3.1 自定义泛型类和接口
泛型类可能有多个参数,此时应将多个参数一起放在尖括号内。比如:<E1,E2,E3>
1 2 3 public interface Map <K, V> { }
泛型类的构造器如下:public GenericClass(){}
。而下面是错误的:public GenericClass<E>(){}
实例化后,操作原来泛型位置的结构必须与指定的泛型类型一致。
泛型不同的引用不能相互赋值。
1 2 3 4 ArrayList<String> list1 = null ; ArrayList<Integer> list2 = null ; list1 = list2;
泛型如果不指定,将被擦除,泛型对应的类型均按照Object处理,但不等价于Object。经验:泛型要使用一路都用。要不用,一路都不要用。
如果泛型结构是一个接口或抽象类,则不可创建泛型类的对象。
jdk1.7,泛型的简化操作:ArrayList<Fruit> flist = new ArrayList<>();
(类型推断)
泛型的指定中不能使用基本数据类型,可以使用包装类 替换。
在类/接口上声明的泛型,在本类或本接口中即代表某种类型,可以作为非静态属性的类型、非静态方法的参数类型、非静态方法的返回值类型。但在静态方法中不能使用类的泛型 。
异常类不能是泛型的。
1 2 3 4 public class MyException <T> extends Exception { }
父类有泛型,子类可以选择保留泛型也可以选择指定泛型类型:
子类不保留父类的泛型:按需实现
子类保留父类的泛型:泛型子类
结论:子类必须是“富二代”,子类除了指定或保留父类的泛型,还可以增加自己的泛型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Father <T1, T2> { } class Son1 extends Father {} class Son2 extends Father <Integer, String> { } class Son3 <T1, T2> extends Father <T1, T2> { } class Son4 <T2> extends Father <Integer, T2> { }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Father <T1, T2> { } class Son <A, B> extends Father { } class Son2 <A, B> extends Father <Integer, String> { } class Son3 <T1, T2, A, B> extends Father <T1, T2> { } class Son4 <T2, A, B> extends Father <Integer, T2> { }
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 public class Order <T> { String orderName; int orderId; T orderT; public Order () { } public Order (String orderName, int orderId, T orderT) { this .orderName = orderName; this .orderId = orderId; this .orderT = orderT; } public T getOrderT () { return orderT; } public void setOrderT (T orderT) { this .orderT = orderT; } @Override public String toString () { return "Order{" + "orderName='" + orderName + '\'' + ", orderId=" + orderId + ", orderT=" + orderT + '}' ; } public static void show () { } }
1 2 3 public class SubOrder extends Order <Integer>{ }
1 2 3 public class SubOrder1 <T> extends Order <T>{ }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Test public void test3 () { Order order = new Order (); order.setOrderT(123 ); order.setOrderT("ABC" ); Order<String> order1 = new Order <>("phone" , 1001 , "这是一部手机" ); order1.setOrderT("又被修改了" ); System.out.println(order1); SubOrder sub1 = new SubOrder (); sub1.setOrderT(123 ); System.out.println(sub1); SubOrder1<String> sub2 = new SubOrder1 <>(); sub2.setOrderT("Sub2..." ); System.out.println(sub2); }
5.3.2 自定义泛型方法 方法,也可以被泛型化,不管此时定义在其中的类是不是泛型类。在泛型方法中可以定义泛型参数,此时,参数的类型就是传入数据的类型。
1 2 3 4 泛型方法的格式: [访问权限] <泛型> 返回类型 方法名([泛型标识 参数名称]) 抛出的异常 例如: public <E> List<E> copyFromArrayToList (E[] arr)
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class Order <T> { String orderName; int orderId; T orderT; public Order () { } public <E> List<E> copyFromArrayToList (E[] arr) { ArrayList<E> list = new ArrayList <>(); for (E e : arr) { list.add(e); } return list; } }
1 2 3 4 5 6 7 @Test public void test4 () { Order<String> order = new Order <>(); Integer[] arr = new Integer []{1 , 2 , 3 , 4 }; List<Integer> list = order.copyFromArrayToList(arr); System.out.println(list); }
5.4 泛型在继承上的体现 如果B是A的一个子类型(子类或者子接口),而G是具有泛型声明的类或接口,G<B>
并不是G<A>
的子类型!
比如:String是Object的子类,但是List<String>
并不是List<Object>
的子类。
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Test public void test5 () { Object obj = null ; String str = null ; obj = str; Object[] arr1 = null ; String[] arr2 = null ; arr1 = arr2; List<Object> list1 = null ; List<String> list2 = null ; }
5.5 通配符
使用类型通配符:?
比如:List<?>
,Map<?,?>
,List<?>
是List<String>
、List<Object>
等各种泛型List的父类 。
读取List<?>
的对象list中的元素时,永远是安全的,因为不管list的真实类型是什么,它包含的都是Object。
写入list中的元素时,不行。因为我们不知道?
的元素类型,我们不能向其中添加对象。唯一的例外是null,它是所有类型的成员。
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 @Test public void test6 () { List<Object> list1= new ArrayList <>(); list1.add("ABC" ); list1.add(123 ); List<String> list2 = null ; List<?> list = null ; list = list1; list = list2; print(list1); List<String> list3 = new ArrayList <>(); list3.add("AA" ); list3.add("BB" ); list3.add("CC" ); list = list3; list.add(null ); Object o = list.get(0 ); System.out.println(o); System.out.println(list); } public void print (List<?> list) { Iterator<?> iterator = list.iterator(); while (iterator.hasNext()) { Object next = iterator.next(); System.out.println(next); } }
6 IO流 6.1 File类 6.1.1 介绍
java.io.File
类:文件和文件目录路径的抽象表示形式,与平台无关
File 能新建、删除、重命名文件和目录,但 File 不能访问文件内容本身。如果需要访问文件内容本身,则需要使用输入/输出流。
想要在Java程序中表示一个真实存在的文件或目录,那么必须有一个File对象,但是Java程序中的一个File对象,可能没有一个真实存在的文件或目录。
File对象可以作为参数传递给流的构造器
6.1.2 常用构造器
public File(String pathname)
:以pathname为路径创建File对象,可以是绝对路径或者相对路径,如果pathname是相对路径,则默认的当前路径在系统属性user.dir
中存储。
public File(String parent, String child)
:以parent为父路径,child为子路径创建File对象。
public File(File parent, String child)
:根据一个父File对象和子文件路径创建File对象
路径分隔符
代码示例
1 2 3 4 5 6 7 8 9 10 @Test public void test0 () { File file1 = new File ("./hello.txt" ); File file2 = new File ("E:\\develop\\study\\" + "backend_study\\javase\\src\\main\\resource\\hello.txt" ); System.out.println(file1); System.out.println(file2); }
6.1.3 常用方法 ① 获取功能
public String getAbsolutePath()
:获取绝对路径
public String getPath()
:获取路径
public String getName()
:获取名称
public String getParent()
:获取上层文件目录路径。若无,返回null
public long length()
:获取文件长度(即:字节数)。不能获取目录的长度。
public long lastModified()
:获取最后一次的修改时间,毫秒值
public String[] list()
:获取指定目录下的所有文件或者文件目录的名称数组
public File[] listFiles()
:获取指定目录下的所有文件或者文件目录的File数组
代码示例1
1 2 3 4 5 6 7 8 9 10 11 12 @Test public void test1 () { File file1 = new File ("hello.txt" ); File file2 = new File ("E:\\develop\\study\\" + "backend_study\\javase\\src\\main\\resources\\hello.txt" ); System.out.println(file1.getAbsolutePath()); System.out.println(file1.getPath()); System.out.println(file1.getName()); System.out.println(file1.getParent()); System.out.println(file1.length()); System.out.println(file1.lastModified()); }
代码示例2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Test public void test2 () { File file = new File ("E:\\develop\\study\\backend_study\\javase" ); String[] list = file.list(); for (String l : list) { System.out.println(l); } File[] files = file.listFiles(); for (File f : files) { System.out.println(f); } }
② 重命名功能
public boolean renameTo(File dest)
:把文件重命名为指定的文件路径
代码示例
1 2 3 4 5 6 7 8 9 @Test public void test3 () { File file1 = new File ("hello.txt" ); File file2 = new File ("E:\\develop\\study\\hi.txt" ); boolean b = file1.renameTo(file2); System.out.println(b); }
执行后,当前路径的hello.txt
已经转移至E:\develop\study\
,且重命名为hi.txt
③ 判断功能
public boolean isDirectory()
:判断是否是文件目录
public boolean isFile()
:判断是否是文件
public boolean exists()
:判断是否存在
public boolean canRead()
:判断是否可读
public boolean canWrite()
:判断是否可写
public boolean isHidden()
:判断是否隐藏
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Test public void test4 () { File file1 = new File ("hello.txt" ); System.out.println(file1.isDirectory()); System.out.println(file1.isFile()); System.out.println(file1.exists()); System.out.println(file1.canRead()); System.out.println(file1.canWrite()); System.out.println(file1.isHidden()); }
④ 创建功能
public boolean createNewFile()
:创建文件。若文件存在,则不创建,返回false
public boolean mkdir()
:创建文件目录。如果此文件目录存在,就不创建了。如果此文件目录的上层目录不存在,也不创建。
public boolean mkdirs()
:创建文件目录。如果上层文件目录不存在,一并创建
⑤ 删除功能
public boolean delete()
:删除文件或者文件夹
删除注意事项:Java中的删除不走回收站。 要删除一个文件目录,请注意该文件目录内不能包含文件或者文件目录
6.2 IO流原理及流的分类 6.2.1 概述 I/O是Input/Output的缩写, I/O技术是非常实用的技术,用于处理设备之间的数据传输。如读/写文件,网络通讯等。
Java程序中,对于数据的输入/输出操作以“流(stream)” 的方式进行。
java.io
包下提供了各种“流”类和接口,用以获取不同种类的数据,并通过标准的方法输入或输出数据。
输入input:读取外部数据(磁盘、光盘等存储设备的数据)到程序(内存)中。硬盘 –> 内存
输出output:将程序(内存)数据输出到磁盘、光盘等存储设备中。 内存 –> 硬盘
应该在程序(内存)的角度看IO流的方向 。
6.2.2 流的分类
按操作数据单位不同分为:字节流(8 bit ),字符流(16 bit )
按数据流的流向不同分为:输入流,输出流
按流的角色的不同分为:节点流,处理流(包在节点流外层)
抽象基类
字节流
字符流
输入流
InputStream
Reader
输出流
OutputStream
Writer
Java的IO流共涉及40多个类,实际上非常规则,都是从如上4个抽象基类派生的。
由这四个类派生出来的子类名称都是以其父类名作为子类名后缀
6.2.3 流的体系
表格中第二行为节点流(或称为文件流),剩余的都为处理流。
6.2.4 节点流和处理流
节点流:直接从数据源或目的地读写数据,或称为文件流
处理流:不直接连接到数据源或目的地,而是“连接”在已存在的流(节点流或处理流)之上,通过对数据的处理为程序提供更为强大的读写功能。
InputStream(典型实现:FileInputStream) 和 Reader(典型实现:FileReader) 是所有输入流的基类。
程序中打开的文件 IO 资源不属于内存里的资源,垃圾回收机制无法回收该资源,所以应该显式关闭文件 IO 资源 。
6.2.6 OutputStream & Writer OutputStream 和 Writer 也非常相似。
因为字符流直接以字符作为操作单位,所以 Writer 可以用字符串来替换字符数组,即以 String 对象作为参数。
FileOutputStream 从文件系统中的某个文件中获得输出字节。FileOutputStream 用于写出非文本数据之类的原始字节流。要写出字符流,需要使用 FileWriter。
6.3 节点流(文件流) 6.3.1 字符流读取文件
Reader:输入字符流
int read()
:读取单个字符 。作为整数读取的字符,范围在 0 到 65535 之间 (0x00-0xffff)(2个字节的Unicode码),如果已到达流的末尾,则返回 -1
int read(char[] cbuf)
:将字符读入数组。如果已到达流的末尾,则返回 -1。否则返回本次读取的字符数。
int read(char[] cbuf,int off,int len)
:将字符读入数组的某一部分。存到数组cbuf中,从off处开始存储,最多读len个字符。如果已到达流的末尾,则返回 -1。否则返回本次读取的字符数。
public void close() throws IOException
:关闭此输入流并释放与该流关联的所有系统资源。
代码示例1
需求:在项目下创建一个文件hello.txt
,文件内容为hello world
,以字符流FileReader
读取该文件到内存中并输出到控制台中显示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 @Test public void test0 () { FileReader fr = null ; try { File file = new File ("hello.txt" ); fr = new FileReader (file); int data; while ((data = fr.read()) != -1 ) { System.out.print((char ) data); } } catch (IOException e) { e.printStackTrace(); } finally { try { if (fr != null ) { fr.close(); } } catch (IOException e) { e.printStackTrace(); } } }
注意:
为了保证流资源一定可以被释放掉,需要使用tcf处理
读入的文件一定要存在,否则会报FileNotFoundException
代码示例2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 @Test public void test1 () { FileReader fr = null ; try { File file = new File ("hello.txt" ); fr = new FileReader (file); char [] cbuf = new char [5 ]; int len; while ((len = fr.read(cbuf)) != -1 ) { String str = new String (cbuf, 0 , len); System.out.print(str); } } catch (IOException e) { e.printStackTrace(); } finally { try { if (fr != null ) { fr.close(); } } catch (IOException e) { e.printStackTrace(); } } }
6.3.2 字符流写入文件
Writer:输出字符流
void write(int c)
:写入单个字符。要写入的字符包含在给定整数值的 16 个低位中,16 高位被忽略。 即写入0 到 65535 之间的Unicode码。
void write(char[] cbuf)
:写入字符数组。
void write(char[] cbuf,int off,int len)
:写入字符数组的某一部分。从off开始,写入len个字符
void write(String str)
:写入字符串。
void write(String str,int off,int len)
:写入字符串的某一部分。
void flush()
:刷新该流的缓冲,则立即将它们写入预期目标。
public void close() throws IOException
:关闭此输出流并释放与该流关联的所有系统资源
代码示例1
需求:从内存中写出数据到硬盘里的文件里。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Test public void test2 () { FileWriter fw = null ; try { File file = new File ("hello1.txt" ); fw = new FileWriter (file); fw.write("I have a dream!\n" ); fw.write("You need to have a dream!" ); } catch (IOException e) { e.printStackTrace(); } finally { try { if (fw != null ) { fw.close(); } } catch (IOException e) { e.printStackTrace(); } } }
注意:
输出的文件对应的file可以不存在
不存在的话会自动创建文件。
如果存在,且流使用的构造器是:FileWriter(file, false)
或FileWriter(file)
,则对原有文件进行覆盖
如果存在,且流使用的构造器是:FileWriter(file, true)
,则对原有文件进行内容追加
代码示例2
需求:读取hello.txt
的内容,并写入到hello1.txt
中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 @Test public void test3 () throws IOException { FileReader fr = null ; FileWriter fw = null ; try { File src = new File ("hello.txt" ); File dest = new File ("hello1.txt" ); fr = new FileReader (src); fw = new FileWriter (dest); char [] cbuf = new char [5 ]; int len; while ((len = fr.read(cbuf)) != -1 ) { fw.write(cbuf, 0 , len); } } catch (IOException e) { e.printStackTrace(); } finally { try { if (fw != null ) fw.close(); } catch (IOException e) { e.printStackTrace(); } try { if (fr != null ) fr.close(); } catch (IOException e) { e.printStackTrace(); } } }
注意点
字符流操作字符,只能操作普通文本文件。最常见的文本文件:.txt,.java,.c,.cpp 等语言的源代码。尤其注意.doc,excel,ppt这些不是文本文件。
6.3.3 字节流读取和写入文件
InputStream:输入字节流
int read()
:从输入流中读取数据的下一个字节。返回 0 到 255 范围内的 int 字节值。如果因为已经到达流末尾而没有可用的字节,则返回值 -1。
int read(byte[] b)
:从此输入流中将最多 b.length 个字节的数据读入一个 byte 数组中。如果因为已经到达流末尾而没有可用的字节,则返回值 -1。否则以整数形式返回实际读取的字节数。
int read(byte[] b, int off, int len)
:将输入流中最多 len 个数据字节读入 byte 数组。尝试读取 len 个字节,但读取的字节也可能小于该值。以整数形式返回实际读取的字节数。如果因为流位于文件末尾而没有可用的字节,则返回值 -1。
public void close() throws IOException
:关闭此输入流并释放与该流关联的所有系统资源。
OutputStream:输出字节流
void write(int b)
:将指定的字节写入此输出流。write 的常规协定是:向输出流写入一个字节。要写入的字节是参数 b 的八个低位。b 的 24 个高位将被忽略。 即写入0~255范围的。
void write(byte[] b)
:将 b.length 个字节从指定的 byte 数组写入此输出流。write(b) 的常规协定是:应该与调用 write(b, 0, b.length) 的效果完全相同。
void write(byte[] b,int off,int len)
:将指定 byte 数组中从偏移量 off 开始的 len 个字节写入此输出流。
public void flush()throws IOException
:刷新此输出流并强制写出所有缓冲的输出字节,调用此方法指示应将这些字节立即写入它们预期的目标。
public void close() throws IOException
:关闭此输出流并释放与该流关联的所有系统资源。
代码示例
需求:实现对图片的复制操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 @Test public void test0 () { FileInputStream fis = null ; FileOutputStream fos = null ; try { File srcFile = new File ("test.png" ); File destFile = new File ("hello.png" ); fis = new FileInputStream (srcFile); fos = new FileOutputStream (destFile); byte [] buffer = new byte [5 ]; int len; while ((len = fis.read(buffer)) != -1 ) { fos.write(buffer, 0 , len); } } catch (IOException e) { e.printStackTrace(); } finally { if (fos != null ) { try { fos.close(); } catch (IOException e) { e.printStackTrace(); } } if (fis != null ) { try { fis.close(); } catch (IOException e) { e.printStackTrace(); } } } }
6.4 处理流 6.4.1 缓冲流 ① 概述 为了提高数据读写的速度 ,Java API提供了带缓冲功能的流类,在使用这些流类时,会创建一个内部缓冲区数组 ,缺省使用8192个字节(8Kb)的缓冲区。
1 2 3 4 5 6 7 public class BufferedInputStream extends FilterInputStream { private static int DEFAULT_BUFFER_SIZE = 8192 ; }
缓冲流要“套接”在相应的节点流 之上,根据数据操作单位可以把缓冲流分为:
BufferedInputStream
和 BufferedOutputStream
,处理字节
BufferedReader
和 BufferedWriter
,处理字符
注意点
当读取数据时,数据按块读入缓冲区,其后的读操作则直接访问缓冲区。
当使用BufferedInputStream读取字节文件时,BufferedInputStream会一次性从文件中读取8192个(8Kb),存在缓冲区中,直到缓冲区装满了,才重新从文件中读取下一个8192个字节数组。
向流中写入字节时,不会直接写到文件,先写到缓冲区中直到缓冲区写满,BufferedOutputStream才会把缓冲区中的数据一次性写到文件里。使用方法flush()可以强制将缓冲区的内容全部写入输出流。
关闭流的顺序和打开流的顺序相反。只要关闭最外层流即可,关闭最外层流也会相应关闭内层节点流 。
flush()方法的使用:手动将buffer中内容写入文件。
如果是带缓冲区的流对象的close()方法,不但会关闭流,还会在关闭流之前刷新缓冲区,关闭后不能再写出。
② 代码示例
代码示例1
需求:利用缓冲流实现非文本文件的复制操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 @Test public void test0 () { BufferedInputStream bis = null ; BufferedOutputStream bos = null ; try { File srcFile = new File ("test.png" ); File destFile = new File ("test1.png" ); FileInputStream fis = new FileInputStream (srcFile); FileOutputStream fos = new FileOutputStream (destFile); bis = new BufferedInputStream (fis); bos = new BufferedOutputStream (fos); byte [] buffer = new byte [10 ]; int len; while ((len = bis.read(buffer)) != -1 ) { bos.write(buffer, 0 , len); } } catch (IOException e) { e.printStackTrace(); } finally { if (bos != null ) { try { bos.close(); } catch (IOException e) { e.printStackTrace(); } } if (bis != null ) { try { bis.close(); } catch (IOException e) { e.printStackTrace(); } } } }
代码示例2
需求:对一个大文件进行复制操作,实现缓冲流和节点流的读写速度对比
略
代码示例3
需求:利用缓冲流实现文本文件的复制操作
注意简写操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @Test public void test1 () { BufferedReader br = null ; BufferedWriter bw = null ; try { br = new BufferedReader (new FileReader (new File ("hello.txt" ))); bw = new BufferedWriter (new FileWriter (new File ("hello1.txt" ))); char [] buffer = new char [10 ]; int len; while ((len = br.read(buffer)) != -1 ) { bw.write(buffer, 0 , len); } } catch (IOException e) { e.printStackTrace(); } finally { if (br != null ) { try { br.close(); } catch (IOException e) { e.printStackTrace(); } } if (bw != null ) { try { bw.close(); } catch (IOException e) { e.printStackTrace(); } } } }
6.4.2 转换流 ① 概述 转换流提供了在字节流和字符流之间的转换。
Java API提供了两个转换流,两者都属于字符流:
InputStreamReader
:将InputStream转换为Reader
OutputStreamWriter
:将Writer转换为OutputStream
字节流中的数据都是字符时,转成字符流操作更高效。 很多时候我们使用转换流来处理文乱码问题。
实现编码和解码(字节/字节数组 –> 字符数组/字符串)的功能。
InputStreamReader
:字符输入流
实现将字节的输入流按指定字符集转换为字符的输入流。
需要和InputStream“套接”。
构造器:
public InputStreamReader(InputStream in)
public InputSreamReader(InputStream in, String charsetName)
OutputStreamWriter
:字符输出流
实现将字符的输出流按指定字符集转换为字节的输出流。
需要和OutputStream“套接”。
构造器:
public OutputStreamWriter(OutputStream out)
public OutputSreamWriter(OutputStream out, String charsetName)
② 字符集
编码表的由来:计算机只能识别二进制数据,早期由来是电信号。为了方便应用计算机,让它可以识别各个国家的文字。就将各个国家的文字用数字来表示,并一一对应,形成一张表。这就是编码表。
常见的编码表:
ASCII
:美国标准信息交换码。用一个字节的7位可以表示。
ISO8859-1
:拉丁码表。欧洲码表用一个字节的8位表示。
GB2312
:中国的中文编码表。最多两个字节编码所有字符
GBK
:中国的中文编码表升级,融合了更多的中文文字符号。最多两个字节编码
Unicode
:国际标准码,融合了目前人类使用的所有字符。为每个字符分配唯一的字符码。所有的文字都用两个字节来表示。
UTF-8
:变长的编码方式,可用1-4个字节来表示一个字符。
面向传输的众多 UTF(UCS Transfer Format)标准出现了,顾名思义,UTF- 8就是每次8个位传输数据,而UTF-16就是每次16个位。这是为传输而设计的编码,并使编码无国界,这样就可以显示全世界上所有文化的字符了。
Unicode只是定义了一个庞大的、全球通用的字符集,并为每个字符规定了唯一确定的编号,具体存储成什么样的字节流,取决于字符编码方案。推荐的Unicode编码是UTF-8和UTF-16。
ANSI编码,通常指的是平台的默认编码,例如英文操作系统中是ISO-8859-1,中文系统是GBK
Unicode字符集只是定义了字符的集合和唯一编号,Unicode编码,则是对UTF-8、UCS-2/UTF-16等具体编码方案的统称而已,并不是具体的编码方案。
③ 代码示例
代码示例1
需求:用字节流(通过字符转换流转换)读取一个txt文件,将读取的内容输出到控制台上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test public void test0 () throws IOException { FileInputStream fis = new FileInputStream (new File ("hello.txt" )); InputStreamReader isr = new InputStreamReader (fis, StandardCharsets.UTF_8); char [] cbuf = new char [20 ]; int len; while ((len = isr.read(cbuf)) != -1 ) { String str = new String (cbuf, 0 , len); System.out.println(str); } isr.close(); }
1 2 hello world你好世界 // UTF-8 hello world浣犲ソ涓栫晫 // gbk
代码示例2
需求:用字符转换流复制文本文件,并更换编码方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Test public void test1 () throws IOException { InputStreamReader isr = new InputStreamReader ( new FileInputStream (new File ("hello.txt" )), StandardCharsets.UTF_8); OutputStreamWriter osw = new OutputStreamWriter ( new FileOutputStream (new File ("hello_gbk.txt" )), "gbk" ); char [] cbuf = new char [20 ]; int len; while ((len = isr.read(cbuf)) != -1 ) { osw.write(cbuf, 0 , len); } isr.close(); osw.close(); }
6.4.3 标准输入输出流
代码示例
需求:从键盘输入字符串,要求将读取到的整行字符串转成大写输出。然后继续进行输入操作,直至当输入“e”或者“exit”时,退出程序。
思路:System.in(字节输入流) --> 转换流 --> BufferedReader的readLine()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public static void main (String[] args) { BufferedReader br = null ; try { InputStreamReader isr = new InputStreamReader (System.in); br = new BufferedReader (isr); while (true ) { System.out.println("请输入字符串: " ); String data = br.readLine(); if ("e" .equalsIgnoreCase(data) || "exit" .equalsIgnoreCase(data)) { System.out.println("程序结束" ); break ; } String upperCase = data.toUpperCase(); System.out.println(upperCase); } } catch (IOException e) { e.printStackTrace(); } finally { if (br != null ) { try { br.close(); } catch (IOException e) { e.printStackTrace(); } } } }
6.4.4 打印流 略
6.4.5 数据流 略
6.4.6 对象流 ① 概述 ObjectInputStream
和OjbectOutputSteam
:用于存储和读取基本数据类型数据或对象的处理流。它的强大之处就是可以把Java中的对象写入到数据源中,也能把对象从数据源中还原回来。
序列化:用ObjectOutputStream类保存 基本类型数据或对象的机制
反序列化:用ObjectInputStream类读取 基本类型数据或对象的机制
ObjectOutputStream和ObjectInputStream不能序列化static和transient修饰的成员变量
对象的序列化
对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流 ,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。当其它程序获取了这种二进制流,就可以恢复成原来的Java对象。
序列化的好处在于可将任何实现了Serializable
接口的对象转化为字节数据,使其在保存和传输时可被还原。
序列化是 RMI(Remote Method Invoke – 远程方法调用)过程的参数和返回值都必须实现的机制,而 RMI 是 JavaEE 的基础。因此序列化机制是JavaEE 平台的基础。
如果需要让某个对象支持序列化机制,则必须让对象所属的类及其属性是可序列化的,为了让某个类是可序列化的,该类必须实现如下两个接口之一。否则,会抛出NotSerializableException
异常
Serializable
Externalizable
凡是实现Serializable接口的类都有一个表示序列化版本标识符 的静态变量:
private static final long serialVersionUID;
serialVersionUID用来表明类的不同版本间的兼容性。简言之,其目的是以序列化对象进行版本控制 ,有关各版本反序列化时是否兼容。
如果类没有显示定义这个静态常量,它的值是Java运行时环境根据类的内部细节自动生成的。若类的实例变量做了修改,serialVersionUID 可能发生变化。故建议显式声明。
简单来说,Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。(InvalidCastException)
② 代码示例
代码示例1
需求:
序列化:将内存中的java对象保存到磁盘中
反序列化:将磁盘中的Java对象读入到内存中
注意写出一次,操作flush()一次
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 @Test public void test0 () { ObjectOutputStream oos = null ; try { oos = new ObjectOutputStream (new FileOutputStream ("object.txt" )); oos.writeObject(new String ("我爱北京天安门" )); oos.flush(); } catch (IOException e) { e.printStackTrace(); } finally { if (oos != null ) { try { oos.close(); } catch (IOException e) { e.printStackTrace(); } } } } @Test public void test1 () { ObjectInputStream ois = null ; try { ois = new ObjectInputStream (new FileInputStream ("object.txt" )); Object o = ois.readObject(); String str = (String) o; System.out.println(str); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } finally { if (ois != null ) { try { ois.close(); } catch (IOException e) { e.printStackTrace(); } } } }
代码示例2
需求:自定义类的序列化和反序列化
1 2 3 4 5 6 7 8 9 @Data @ToString @AllArgsConstructor public class Person implements Serializable { public static final long serialVersionUID = 12345678910L ; private String name; private int age; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 @Test public void test0 () { ObjectOutputStream oos = null ; try { oos = new ObjectOutputStream (new FileOutputStream ("object.dat" )); oos.writeObject(new String ("我爱北京天安门" )); oos.flush(); oos.writeObject(new Person ("Mark" , 24 )); oos.flush(); } catch (IOException e) { e.printStackTrace(); } finally { if (oos != null ) { try { oos.close(); } catch (IOException e) { e.printStackTrace(); } } } } @Test public void test1 () { ObjectInputStream ois = null ; try { ois = new ObjectInputStream (new FileInputStream ("object.dat" )); Object o = ois.readObject(); String str = (String) o; System.out.println(str); Person p = (Person) ois.readObject(); System.out.println(p); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } finally { if (ois != null ) { try { ois.close(); } catch (IOException e) { e.printStackTrace(); } } } }
结果:
1 2 我爱北京天安门 Person(name=Mark, age=24)
注意:如果某个类的属性不是基本数据类型或 String 类型,而是另一个引用类型,那么这个引用类型必须是可序列化的,否则拥有该类型的Field的类也不能序列化。
6.4.7 随机存取文件流 ① 概述
RandomAccessFile 声明在java.io包下,但直接继承于java.lang.Object类。并且它实现了DataInput、DataOutput这两个接口,也就意味着这个类既可以读也可以写 。
RandomAccessFile 类支持 “随机访问” 的方式,程序可以直接跳到文件的任意地方 来读、写文件
支持只访问文件的部分内容
可以向已存在的文件后追加内容
RandomAccessFile 对象包含一个记录指针,用以标示当前读写处的位置。RandomAccessFile 类对象可以自由移动记录指针。
long getFilePointer()
:获取文件记录指针的当前位置
void seek(long pos)
:将文件记录指针定位到 pos 位置
构造器:
public RandomAccessFile(File file, String mode)
public RandomAccessFile(String name, String mode)
创建 RandomAccessFile 类实例需要指定一个 mode
参数,该参数指定 RandomAccessFile 的访问模式:
r
: 以只读方式打开
rw
:打开以便读取和写入
rwd
:打开以便读取和写入;同步文件内容的更新
rws
:打开以便读取和写入;同步文件内容和元数据的更新
如果模式为只读r,则不会创建文件,而是会去读取一个已经存在的文件,如果读取的文件不存在则会出现异常。
如果模式为rw读写。如果文件不存在则会去创建文件,如果存在则不会创建,且默认情况下对原内容进行覆盖操作。
我们可以用RandomAccessFile这个类,来实现一个多线程断点下载的功能,用过下载工具的朋友们都知道,下载前都会建立两个临时文件,一个是与被下载文件大小相同的空文件,另一个是记录文件指针的位置文件,每次暂停的时候,都会保存上一次的指针,然后断点下载的时候,会继续从上一次的地方下载,从而实现断点下载或上传的功能,有兴趣的朋友们可以自己实现下。
② 代码示例
代码示例1
需求:复制图片
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @Test public void test0 () { RandomAccessFile raf1 = null ; RandomAccessFile raf2 = null ; try { raf1 = new RandomAccessFile (new File ("test.png" ), "r" ); raf2 = new RandomAccessFile (new File ("test2.png" ), "rw" ); byte [] buffer = new byte [1024 ]; int len; while ((len = raf1.read(buffer)) != -1 ) { raf2.write(buffer, 0 , len); } } catch (IOException e) { e.printStackTrace(); } finally { if (raf2 != null ) { try { raf2.close(); } catch (IOException e) { e.printStackTrace(); } } if (raf1 != null ) { try { raf1.close(); } catch (IOException e) { e.printStackTrace(); } } } }
代码示例2
需求:给定一个字符文件,内容为abcdefghijklmn
,要求在abc
后插入(且不覆盖后面的字符)字符xyz
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Test public void test1 () throws IOException { RandomAccessFile raf1 = new RandomAccessFile ("hello.txt" , "rw" ); raf1.seek(3 ); StringBuilder sb = new StringBuilder ((int ) new File ("hello.txt" ).length()); byte [] buffer = new byte [1024 ]; int len; while ((len = raf1.read(buffer)) != -1 ) { sb.append(new String (buffer, 0 , len)); } raf1.seek(3 ); raf1.write("xyz" .getBytes()); raf1.write(sb.toString().getBytes()); raf1.close(); }
6.5 NIO 略,详见实用技术-Netty学习笔记
7 网络编程 略,详见SpringMVC学习笔记
和Java Web学习笔记
8 反射机制 8.0 Java程序编译和运行过程 8.0.1 流程和JVM内存模型
编辑源代码(.java
)
将源代码.java
编译 成字节码文件 .class
JVM
对字节码文件进行解析执行(加载进内存执行),输出结果。
在jvm中,通过类加载器(ClassLoader
)将字节码文件(.class
)加载进内存,java解释器 将字节码翻译成对应的机器码 ,最后在操作系统解释运行。
线程共享
主要包括:常量、静态变量、类信息(Class类型的对象) 、运行时常量池,操作的是直接内存。各个线程共享。
实际上,方法区在物理上是堆的一部分,但是《Java虚拟机规范》在实现上更支持将方法区从堆逻辑中划分出来成为一块独立的内存区。
存储该程序在运行时所有创建的对象(或称为实例)。一个JVM实例只存在一个堆内存,所有线程共享,包含新生区、养老区、永久区。
线程独占
每个线程都在这个空间有一个私有的空间。线程栈由多个栈帧Stack Frame
组成。栈帧内容包含:局部变量 表、操作数栈、动态链接、方法返回地址、附加信息等。
和虚拟机栈功能类似,虚拟机栈是为虚拟机执行JAVA方法而准备的,本地方法栈是为虚拟机使用Native
本地方法而准备的。
记录当前线程执行字节码的位置,存储的是字节码指令地址,如果执行Native方法,则计数器值为空。
8.0.2 相关命令
javac
(java compile
)命令用于将Java源代码编译成Java字节码文件(.class
文件),以便在Java虚拟机(JVM)上运行。
语法格式:
1 javac [options] source_file(s)
java
命令用于启动Java虚拟机并执行Java程序。使用java命令可以在命令行中直接运行编译后的Java程序。
语法格式:
1 java [options] class [args...]
options
:Java命令提供了多个选项来控制Java虚拟机和应用程序的行为。可以使用java -help查看所有选项及其说明。
class
:要运行的Java类名。
args…
:传递给主方法的参数。这些参数将作为字符串数组传递给main()
方法。
示例
新建Test.java
,注意文件名必须和类名相同!
1 2 3 4 5 6 7 8 public class Test { public static void main (String[] args) { System.out.println("Hello World!" ); for (String s : args) { System.out.println(s); } } }
生成了字节码文件Test.class
8.1 反射机制概述 8.1.1 概述
Reflection(反射)是被视为动态语言的关键,反射机制允许程序在执行期间借助于Reflection API
取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。
加载完类之后,在JVM内存的方法区 中就产生了一个Class
类型的对象(一个类只有一个Class对象),这个对象就包含了完整的类的结构信息。我们可以通过这个对象看到类的结构。这个对象就像一面镜子,透过这个镜子看到类的结构,所以,我们形象的称之为:反射。
8.1.2 与反射有关的API
java.lang.Class
:代表一个类
java.lang.reflect.Method
:代表类的方法
java.lang.reflect.Field
:代表类的成员变量
java.lang.reflect.Constructor
:代表类的构造器
8.1.3 利用反射之前对Person类的操作
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class Person { private String name; public int age; public void show () { System.out.println("我是一个人" ); } private String showNation (String nation) { System.out.println("我的国籍是" +nation); return nation; } private Person (String name) { this .name = name; } public Person (String name, int age) { this .name = name; this .age = age; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test public void test () { Person p1 = new Person ("Tom" ,22 ); p1.setAge(24 ); System.out.println(p1); p1.show(); }
8.1.4 利用反射对Person类操作
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 @Test public void test1 () throws Exception{ Class clazz = Person.class; Constructor cons = clazz.getConstructor(String.class, int .class); Object obj = cons.newInstance("Tom" , 23 ); Person p = (Person)obj; System.out.println(p); Field age = clazz.getDeclaredField("age" ); age.set(p,10 ); System.out.println(p); Method show = clazz.getDeclaredMethod("show" ); show.invoke(p); Constructor cons1 = clazz.getDeclaredConstructor(String.class); cons1.setAccessible(true ); Person p1 = (Person) cons1.newInstance("Jerry" ); System.out.println(p1); Field name = clazz.getDeclaredField("name" ); name.setAccessible(true ); name.set(p1,"HanMeimei" ); System.out.println(p1); Method showNation = clazz.getDeclaredMethod("showNation" , String.class); showNation.setAccessible(true ); String nation = (String)showNation.invoke(p1,"中国" ); System.out.println(nation); }
8.1.5 如何看待反射和封装性
通过直接new对象,或反射的方式都可以调用公共的结构,那么开发中到底用哪个?
回答:建议直接用new的方法。
回答:不矛盾。
8.2 理解Class类并获取Class实例 8.2.1 Class类的理解
程序在javac.exe
命令(编译)后,会生成一个或多个字节码文件(.class
结尾),接着使用java.exe
命令(运行)对某个字节码文件进行解释运行,相当于将某个字节码文件加载到内存中,此过程就称为==类的加载==。加载到内存中的类,就成为==运行时类==,此运行时类就作为Class类的一个实例对象 。
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Class c1 = Object.class; Class c2 = Comparable.class;Class c3 = String[].class;Class c4 = int [][].class;Class c5 = ElementType.class;Class c6 = Override.class;Class c7 = int .class;Class c8 = void .class;Class c9 = Class.class;int [] a = new int [10 ];int [] b = new int [100 ];Class c10 = a.getClass();Class c11 = b.getClass();System.out.println(c10 == c11);
8.2.2 获取Class实例的四种方法
调用运行时类的属性:.class
通过运行时类的对象,调用getClass()
方法
调用Class的静态方法:forName(String classPath)
用的最多
使用类加载器:ClassLoader
了解,用得少
代码实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @Test public void test2 () throws ClassNotFoundException { Class<Person> clazz = Person.class; System.out.println(clazz); Person p1 = new Person (); Class clazz2 = p1.getClass(); System.out.println(clazz2); Class clazz3 = Class.forName("com.hongyi.Person" ); System.out.println(clazz3); System.out.println(clazz == clazz2); System.out.println(clazz2 == clazz3); ClassLoader classLoader = ReflectionTest.class.getClassLoader(); Class clazz4 = classLoader.loadClass("com.hongyi.Person" ); System.out.println(clazz4); System.out.println(clazz4 == clazz); }
8.2.3 Class类常用方法
8.3 类的加载与ClassLoader的理解 8.3.1 类的加载过程 当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过如下三个步骤来对该类进行初始化:
验证:确保加载的类信息符合JVM规范,例如:以cafe开头,没有安全方面的问题。
准备:正式为类变量(static)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配。
解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。
执行类构造器<clinit>()
方法的过程。类构造器<clinit>()
方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器)。
当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
虚拟机会保证一个类的<clinit>()
方法在多线程环境中被正确加锁和同步
8.3.2 ClassLoader的理解
类加载器
类加载器的作用:将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆(准确来说是在方法区) 中生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口。
类缓存:标准的JavaSE类加载器可以按要求查找类,但一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间。不过JVM垃圾回收机制可以回收这些Class对象。
ClassLoader
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Test public void test3 () { ClassLoader classLoader = ReflectionTest.class.getClassLoader(); System.out.println(classLoader); ClassLoader classLoader1 = classLoader.getParent(); System.out.println(classLoader1); ClassLoader classLoader2 = classLoader1.getParent(); System.out.println(classLoader2); ClassLoader classLoader3 = String.class.getClassLoader(); System.out.println(classLoader3); }
使用ClassLoader
加载配置文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Test public void test4 () throws Exception { Properties prop = new Properties (); FileInputStream fis = new FileInputStream ("jdbc.properties" ); prop.load(fis); ClassLoader classLoader = ReflectionTest.class.getClassLoader(); InputStream is = classLoader.getResourceAsStream("jdbc1.properties" ); prop.load(is); System.out.println(prop.getProperty("user" )); System.out.println(prop.getProperty("password" )); }
8.4 创建运行时类的对象 8.4.1 newInstance() 此方法实质上是调用运行时类的空参构造器,要想此方法正确的创建运行时类的对象,要求:
运行时类必须提供空参构造器
空参构造器的访问权限必须得够,通常设置为public
在Spring
中要求提供一个空参构造器,便于通过反射来创建运行时类对象,便于子类继承此运行时类时,默认调用super()
时,保证父类有此构造器。
代码演示
1 2 3 4 5 6 7 8 9 @Test public void test5 () throws IllegalAccessException, InstantiationException { Class clazz = Person.class; Person p = (Person) clazz.newInstance(); System.out.println(p); }
8.4.2 反射的动态性理解 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @Test public void test6 () { int num = new Random ().nextInt(3 ); String classPath = "" ; switch (num){ case 0 : classPath = "java.util.Date" ; break ; case 1 : classPath = "java.lang.Object" ; break ; case 2 : classPath = "com.hongyi.Person" ; break ; } try { Object obj = getInstance(classPath); System.out.println(obj); } catch (Exception e) { e.printStackTrace(); } } public Object getInstance (String classPath) throws Exception{ Class clazz = Class.forName(classPath); return clazz.newInstance(); }
8.5 获取运行时类的完整结构 8.5.1 代码准备 1 2 3 4 5 6 7 8 9 10 11 12 public class Creature <T> implements Serializable { private char gender; public double weight; private void breath () { System.out.println("生物呼吸" ); } public void eat () { System.out.println("生物进食" ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 @MyAnnotation(value = "hi") public class Human extends Creature <String> implements Comparable <String>,MyInterface{ private String name; int age; public int id; public Human () { } @MyAnnotation(value = "ABC") private Human (String name) { this .name = name; } Human(String name,int age){ this .name = name; this .age = age; } @MyAnnotation private String show (String nation) { System.out.println("我的国籍是:" +nation); return nation; } public String display (String interests) { return interests; } public void info () { System.out.println("我是一个人" ); } public int compareTo (String s) { return 0 ; } }
1 2 3 public interface MyInterface { void info () ; }
1 2 3 4 5 @Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.CONSTRUCTOR, ElementType.LOCAL_VARIABLE}) @Retention(RetentionPolicy.RUNTIME) public @interface MyAnnotation { String value () default "hello" ; }
8.5.2 获取运行时类的属性结构和内部结构
getFields()
:获取当前运行时类及其父类中声明为public的属性
getDeclaredFields()
:获取当前运行时类的所有属性,不包含父类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Test public void test7 () { Class clazz = Human.class; Field[] fields = clazz.getFields(); for (Field f : fields){ System.out.println(f); } System.out.println(); Field[] declaredFields = clazz.getDeclaredFields(); for (Field f : declaredFields){ System.out.println(f); } }
getModifiers()
getType()
getName()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Test public void test8 () { Class clazz = Human.class; Field[] declaredFields = clazz.getDeclaredFields(); for (Field f : declaredFields){ int modifiers = f.getModifiers(); System.out.print("权限修饰符:" +Modifier.toString(modifiers)+" " ); Class type = f.getType(); System.out.print("数据类型:" +type.getName()+" " ); String name = f.getName(); System.out.println("变量名:" +name); System.out.println(); } }
8.5.3 获取运行时类的方法结构 1 2 3 4 5 public Method[] getDeclaredMethods()public Method[] getMethods()
8.6 调用运行时类的指定结构 8.6.1 调用指定方法 通过反射,调用类中的方法,通过Method类完成。
1 2 3 4 5 Object invoke (Object obj, Object... args) ;
示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class InvokeTest { public void test (String[] arg) { for (String string : arg) { System.out.println(string); } } public static void main (String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { Class<InvokeTest> clazz = InvokeTest.class; String[] s = new String []{"Hello" ,"World" }; Method method = clazz.getMethod("test" , String[].class); method.invoke(new InvokeTest (), (Object) s); } }
执行结果:
1 2 3 4 Hello World Process finished with exit code 0
8.6.2 调用指定属性 略
8.7 反射的应用——动态代理 代理模式:使用一个代理将对象包装起来, 然后用该代理对象取代原始对象。任何对原始对象的调用都要通过代理。代理对象决定是否以及何时将方法调用转到原始对象上。
动态代理是指客户通过代理类来调用其它对象的方法,并且是在程序运行时根据需要动态创建目标类的代理对象。
8.7.1 动态代理相关API Proxy
:专门完成代理的操作类,是所有动态代理类的父类。通过此类为一个或多个接口动态地生成被代理类。
静态方法:
1 2 3 4 5 6 7 8 static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces);static Object newProxyInstance (ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) ;
8.7.2 动态代理示例
问题1:如何根据加载到内存中的被代理类,动态创建一个代理类及其对象
问题2:当通过代理类的对象调用方法时,如何动态去调用被代理类中的同名方法
顶层接口
1 2 3 4 5 public interface Human { String getBelief () ; void eat (String food) ; }
实现类,且是被代理类
1 2 3 4 5 6 7 8 9 10 11 12 public class SuperMan implements Human { @Override public String getBelief () { return "I believe I can fly!" ; } @Override public void eat (String food) { System.out.println("I like eating " + food); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class ProxyFactory { public static Object getProxyInstance (Object obj) { MyInvocationHandler handler = new MyInvocationHandler (); handler.bind(obj); return Proxy.newProxyInstance( obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), handler ); } }
MyInvocationHandler
是一个实现接口InvocationHandler
的类,它必须实现invoke
方法,以完成代理的具体操作,解决了问题2。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class MyInvocationHandler implements InvocationHandler { private Object obj; public void bind (Object obj) { this .obj = obj; } @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { return method.invoke(obj, args); } }
1 2 3 4 5 6 7 8 9 10 public class ProxyTest { public static void main (String[] args) { SuperMan superMan = new SuperMan (); Human proxyInstance = (Human) ProxyFactory.getProxyInstance(superMan); System.out.println(proxyInstance.getBelief()); proxyInstance.eat("banana" ); } }
执行结果:
1 2 I believe I can fly! I like eating banana
可以使用lambda表达式替换MyInvocationHandler
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class ProxyFactory { public static Object getProxyInstance (Object obj) { return Proxy.newProxyInstance( obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), (proxy, method, args) -> { System.out.println("方法增强..." ); return method.invoke(obj, args); } ); } }
执行结果:
1 2 I believe I can fly! I like eating banana