天天看点

【Java基础】Java反射机制、正则表达式、比较器、GC垃圾回收、StringBuffer类、对象克隆、日期处理

《第一行代码:Java》第10章、Java常用类库 读书笔记

文章目录

    • 第10章、Java常用类库
      • 10.1 StringBuffer类
      • 10.2 Runtime类
        • Runtime类方法
        • 内存划分与GC垃圾回收
      • 10.3 System类
      • 10.4 对象克隆
      • 10.5 数字操作类
        • Math类
        • Random类
        • 大数操作类
      • 10.6 日期处理类
        • Date类
        • 日期格式化:SimpleDateFormat
        • Calendar类
      • 10.7 比较器
        • Arrays类
        • 比较器:Comparable
        • 数据结构——BinaryTree(二叉树)
        • 挽救的比较器:Comparator
      • 10.8 正则表达式
        • 正则标记
        • String类对正则的支持
      • 10.9 反射机制
        • 认识反射
        • Class类对象实例化
        • 反射实例化对象
        • 利用反射实现工厂设计模式
        • 反射调用构造
        • 反射调用方法
        • 反射调用成员
        • 反射机制总结
      • 本章小结

第10章、Java常用类库

10.1 StringBuffer类

在Java中,字符串使用String类进行表示,但String类表示的字符串一旦声明就无法改变。所以String字符串不适合用于需要频繁修改的字符串操作上,所以对于这种情况Java中提供了StringBuffer类字符串方便用户修改。

  • String与StringBuffer定义的区别:
    • String类的定义:
      public final class String extends Object implements Serializable, Comparable<String>, CharSequence
                 
    • StringBuffer类定义:
      public final class StringBuffer extends Object implements Serializable, CharSequence
                 
    通过两个类的定义结构可以发现,String类与StringBuffer类都是CharSequence接口的子类,也就是说String类与StringBuffer类对象都可以向上转型为CharSequence接口实例化。
  • 取得CharSequence接口实例化对象:
    package com.yootk.demo;
    public class TestDemo {
    	public static void main(String[] args) throws Exception {
    		CharSequence seq1 = "String类字符串"; 							// 向上转型
            CharSequence seq2 = new StringBuffer("StringBuffer类字符串");	// 向上转型
    		System.out.println(seq1); 				// String类覆写的toString()
            System.out.println(seq2); 				// StringBuffer类覆写的toString()
    	}
    }
               
  • 虽然String与StringBuffer类都属于CharSequence接口的子类,但这两个类的对象不能直接转换。
  • String转StringBuffer类对象:
    • 方式一、利用StringBuffer类的构造方法:public StringBuffer(String str)
    • 方式二、利用StringBuffer类中的append()方法:public StringBuffer append(String str)
      StringBuffer buf = new StringBuffer();
      buf.append("java").append("学习");
                 
  • StringBuffer转String类对象:
    • 方式一、利用String类的构造方法:public String(StringBuffer buffer)
      StringBuffer buf = new StringBuffer("java学习");
      String str = new String(buf)
                 
    • 方式二、利用StringBuffer类的toString()方法:public String toString()
      StringBuffer buf = new StringBuffer("java学习");
      String str = buf.toString();
                 
  • String类对象与StringBuffer类对象内容的比较:

    在String类中提供了一个与StringBuffer比较的方法:public boolean contentEquals(StringBuffer sb)

    package com.yootk.demo;
    public class TestDemo {
    	public static void main(String[] args) throws Exception {
    		StringBuffer buf = new StringBuffer("yootk");
    		System.out.println("yootk".contentEquals(buf));	
    	}
    }
    // 程序执行结果:	true
               
  • StringBuffer类的常用操作方法:
    public StringBuffer append(数据类型变量)					// 数据最佳操作,可接收8种基本数据类型和String、StringBuffer类字符串
    public StringBuffer reverse()							// 字符串反转操作
    public StringBuffer insert(int offset, 数据类型 变量名)	// 在指定位置追加内容,可接收8种基本数据类型和String、StringBuffer类字符串
    public StringBuffer delete(int Start, int end)			// 删除指定索引范围的内容
               
    package com.yootk.demo;
    public class TestDemo {
    	public static void main(String[] args) throws Exception {
    		StringBuffer buf = new StringBuffer();
            buf.append("www..com").insert(4, "baidu").reverse().delete(4, 8);
    		System.out.println(buf);
        }
    }
    // 程序执行结果:	moc..www
               
    由于上面所有方法返回值都是StringBuffer类对象,所有可以使用代码链的方式调用方法,例如:对象.append(“xxx”).insert()…
  • StringBuilder类:

    从JDK1.5开始,Java中增加了一个新类:StringBuilder类,其定义结构如下:

    public final class StringBuilder extends Object implements Serializable, CharSequence
               
    通过定义结构发现,StringBuilder类与Stringbuffer类是完全相同的。而你去查看JDK源码后会发现,这两个类中的方法功能是一样的,唯一的区别是StringBuffer类中的所有方法都用关键字“synchronized”进行同步定义,而StringBuilder类中没有进行同步定义,所以StringBuilder类的方法都是异步的。

10.2 Runtime类

Runtime类方法

在每一个JVM进程中,都会存在一个运行状态的操作类的对象,而这个对象所属类型就是Runtime类。利用Runtime类可以启动新的进程或进行运行环境的操作,如:取得内存空间大小和释放垃圾空间。

  • Runtime类使用了单例设计模式(见5.7单例设计模式),所以无法通过构造方法获得其实例化对象,所以Runtime中提供了一个static方法getRuntime()用来取得Runtime类的实例化对象。
  • Runtime类中的常用方法:
    public static Runtime getRuntime()						// 普通,取得Runtime类实例化对象
    public long maxMemory()									// 普通,返回最大可用内存大小,单位Byte(字节)
    public long totalMemory()								// 普通,返回所有可用内存大小,单位Byte(字节)
    public long freeMemory()								// 普通,返回空余内存大小,单位Byte(字节)
    public void gc()										// 普通,执行垃圾回收操作
    public Process exec(String command) throws IOException	// 普通,创建新的进程
               
    package com.yootk.demo;
    public class TestDemo {
    	public static void main(String[] args) throws Exception {
            Runtime run = Runtime.getRuntime(); 					// 取得Runtime类的实例化对象
            String str = "";
            for (int x = 0; x < 2000; x++) {
                str += x; 							                // 产生大量垃圾
            }
            System.out.println("【垃圾处理前最大可用内存量】MAX = " + run.maxMemory());
            System.out.println("【垃圾处理前所有可用内存量】TOTAL = " + run.totalMemory());
            System.out.println("【垃圾处理前所有空闲内存量】FREE = " + run.freeMemory());
            run.gc(); 							// 释放垃圾空间
            System.out.println("【垃圾处理后最大可用内存量】MAX = " + run.maxMemory());
            System.out.println("【垃圾处理后所有可用内存量】TOTAL = " + run.totalMemory());
            System.out.println("【垃圾处理后所有空闲内存量】FREE = " + run.freeMemory());
    	}
    }
    /*
    程序执行结果:
        【垃圾处理前最大可用内存量】MAX = 3799515136
        【垃圾处理前所有可用内存量】TOTAL = 257425408
        【垃圾处理前所有空闲内存量】FREE = 200972864
        【垃圾处理后最大可用内存量】MAX = 3799515136
        【垃圾处理后所有可用内存量】TOTAL = 257425408
        【垃圾处理后所有空闲内存量】FREE = 255381872
    */
               
    本程序使用for循环产生了大量垃圾空间,然后调用gc方法进行垃圾回收,时可用的空闲空间大大增加。

内存划分与GC垃圾回收

  • Java内存划分:
    【Java基础】Java反射机制、正则表达式、比较器、GC垃圾回收、StringBuffer类、对象克隆、日期处理
    每一块内存空间都会存在一个内存伸缩区,当内存不足时就会动态开辟。
  • 垃圾回收过程:
    • 虽然垃圾收集只需通过一个gc()方法完成,但垃圾回收与Java的内存划分也是有关系的。
      • 因为垃圾回收主要是对年轻代(Young Generation)与旧生代(Old Generation)的内存进行回收。
      • 年轻代内存空间用于存放新产生的对象,而经过若干次回收还没有被回收掉的对象向旧生代内存空间移动。
      • 对年轻代进行垃圾回收称为MinorGC(从垃圾收集),对旧生代垃圾回收称为MajorGC(主垃圾收集),并且两块内存回收互不干涉。
      • 在JVM中的对象回收机制会使用分代回收(Generational Collection)的策略,用较高的频率对年轻代对象进行扫描和回收,而对旧生代对象则用较低的频率进行回收,这样就不需要在每次执行GC时将内存中的所有对象都检查一遍。
    • 对于GC的执行可以用文字描述为:当JVM剩余内存空间不足时就会自动除法GC或直接调用Runtime类中的gc()方法手工回收(手动gc()是进行完全垃圾回收)。
      • Eden内存空间不足就要进行从回收(Minor Collection);
      • 旧生代空间不足时就进行主回收(Major Collection);
      • 永久代空间不足就进行完全垃圾回收(Full Collection)。
  • 在清楚JVM的内存分配后,就可以进一步理解对象创建流程,如图10-3所示:
    【Java基础】Java反射机制、正则表达式、比较器、GC垃圾回收、StringBuffer类、对象克隆、日期处理
    • 当使用关键字new创建一个新对象时,JVM 会将新对象保存在Eden 区,但是此时需要判断Eden 区是否有空余空间,如果有,则直接将新对象保存在Eden区内,如果没有,则会执行“Minor GC”(年轻代 GC)。
    • 在执行完“Minor GC”后会清除掉不活跃的对象,从而释放Eden区的内存空间,随后会对 Eden空间进行再次判断。如果此时剩余空间可以直接容纳新对象,则会直接为新对象申请内存空间;如果此时 Eden区的空间依然不足,则会将部分活跃对象保存在 Survivor区。
    • 由于Survivor区也有对象会存储在内,所以在保存Eden 区发送来的对象前首先需要判断其空间是否充足,如果Survivor有足够的空余空间,则直接保存 Eden 区晋升来的对象,那么此时Eden区将得到空间释放,随后可以在Eden 区为新的对象申请内存空间的开辟;如果Survivor 区空间不足,就需要将Survivor区的部分活跃对象保存到Tenured区。
    • Tenured区如果有足够的内存空间,则会将Survivor区发送来的对象进行保存,如果此时Tenured 区的内存空间也已经满了,则将执行“Full GC”(完全GC 或称为“Major GC”,包括年轻代和老年代,相当于使用 “Runtime.getRuntime().gc()” 处理,以释放老年代中保存的不活跃对象。如果在释放后有足够的内存空间,则会保存Survivor 发送来的对象,从而Survivor 将保存 Eden 发送来的对象,这样就可以在Eden区内有足够的内存保存新的对象。
    • 如果此时老年代的内存区也已经被占满,则会抛出 “OutOfMemoryError” (OOM错误),程序将中断运行。

10.3 System类

  • System类的常用方法:
    public void arraycopy(Object stc, int srcPos, Object dest, int destPos, intlength)	// 数组复制
    public static long currentTimeMillis()												// 取得当前日期时间
    public static void gc()																// 垃圾收集
               
  • System类中的垃圾回收方法gc()并不是新操作,而是间接调用Runtime类中的gc()方法,不是方法重写。所以调用System.gc()和Runtime.gc()效果设计一样的。
  • 对象的回收:我们可以发现Java中并没有像C++中一样拥有析构函数,所有当我们向回收一个对象时,则对象所在的类可以通过finalize()方法实现,该方法会在对象被回收时自动调用。此方法由Object类定义:
    protected void finalize() throws Throwable
               
  • 对象回收操作:
    package com.yootk.demo;
    class Human {
    	public Human() {
    		System.out.println("调用构造方法");
    	}
    	@Override
        protected void finalize() throws Throwable {		// 覆写Object类方法
    		System.out.println("对象被回收");
    		throw new Exception("此处即使抛出异常对象也不会产生任何影响!");
    	}
    }
    public class TestDemo {
    	public static void main(String[] args) {
    		Human mem = new Human(); 						// 实例化新的对象
    		mem = null; 									// 产生垃圾
    		System.gc(); 									// 手工处理垃圾收集
    	}
    }
    /*
    程序执行结果:
        调用构造方法
        对象被回收
    */
               
    当对象被回收时会自动调用finalize()方法,这样就可以对一些对象回收前进行收尾操作。并且此方法即使产生任何异常或错误也不会影响程序的正常执行。
  • final、finally和finalize的区别:
    • final:表示终结器,用于定义不能被继承的父类,不能被覆写的方法、变量。
    • finally:异常处理的出口
    • finalize:Object类定义的一个方法,用于执行对象回收的收尾操作,在对象被回收时自动调用。

10.4 对象克隆

  • 对象克隆就是对象的复制操作,在Object类中存在一个clone()方法用于对象的克隆:

    此方法是实现克隆的唯一方法,但此方法是使用protected声明的,这样不同包的类产生的对象就无法调用Object类中的clone()方法,因此需要子类来覆写clone()方法(但调用的任然是父类中的clone方法),才能正常完成克隆操作。

  • Cloneable接口:只有实现了Cloneable接口的类的对象才能进行克隆,负责会抛出CloneNotSupportedException异常。这个接口属于标识接口,用于表示一种能力。
  • 实现克隆操作:
    package com.yootk.demo;
    class Book implements Cloneable { 					// 实现Cloneable接口,表示此类的对象可以被克隆
    	private String title;
    	private double price;
        public Book() {}
    	public Book(String title, double price) {
    		this.title = title;
    		this.price = price;
    	}
    	public void setTitle(String title) {
    		this.title = title;
    	}
    	@Override
    	public String toString() {
    		return "书名:" + this.title + ",价格:" + this.price;
    	}
    	// 由于此类需要对象克隆操作,所以才需要进行方法的覆写
    	@Override
    	public Object clone() throws CloneNotSupportedException {
    		return super.clone(); 							// 调用父类的克隆方法
    	}
    }
    public class TestDemo {
    	public static void main(String[] args) throws Exception {
            Book bookA = new Book("Java开发", 79.8);			// 实例化新对象
            Book bookB = (Book) bookA.clone();				 // 克隆对象,开辟新的堆内存空间
            Book bookC = bookA;								 // 使用“ = ”属于引用赋值
            bookB.setTitle("JSP开发");						// 修改克隆对象属性,不影响其他对象
            System.out.println(bookA);
            bookC.setTitle("C++开发");						// 修改引用对象属性,会影响被引用对象
            System.out.println(bookA);
    	}
    }
    /*
    程序执行结果:
    	书名:Java开发,价格:79.8
    	书名:C++开发,价格:79.8
    */
               
    克隆出的新对象与被克隆的对象属性相同,但并不是同一个对象,两个对象占据着不同的堆空间,彼此间不会相互印象。而使用 “ = ” 赋值,属于引用传递,属于同一个对象,不同的名字而已。

10.5 数字操作类

Java中为了方便进行数学计算,专门提供了 java.lang.Math 类,但Math类所能完成的操作有限。所以在 java.math 包中专门提供了负责大数字的操作。

Math类

Math类就是一个专门进行数学计算的操作类,在Math类中提供的所有方法都是static型的方法,所以可以直接使用类名调用:Math.方法()

Random类

java.util.Random是一个专门负责产生随机数的操作类,此类的常用方法:

public Random()						// 构造,创建一个新的Random实例
public Random(long seed)			// 构造,创建一个新的Random实例并设置一个种子数
public int nextInt(int bound)		// 普通,产生一个不大于指定边界的随机整数
           

大数操作类

如果有两个非常大的数要进行数学操作(这时数字已经超过double的范围),那么只能由字符串来表示,然后再取出进行数学运算,但这种方法难度较高。所以Java中提供了两个大数字操作类:java.math.BigInteger和java.math.BigDecimal,而这两个类都属于Number的子类。

  • 大整形操作类:BIgInteger
    【Java基础】Java反射机制、正则表达式、比较器、GC垃圾回收、StringBuffer类、对象克隆、日期处理
  • 大小数操作类:BigDecimal(除了拥有与BigInteger同样的基本计算方式,还拥有以下操作)
    【Java基础】Java反射机制、正则表达式、比较器、GC垃圾回收、StringBuffer类、对象克隆、日期处理

10.6 日期处理类

Date类

  • 在Java中,如果要表示日期型,需要使用java.ueil.Date类,该类中的主要常用方法:
    public Date()					// 构造,实例化Date类对象,默认值为当前时间
    public Date(long date)			// 将long型的日期时间数据变成Date类对象,
    public long getTime()			// 将当前的日期时间变成long型
               
  • Date与long的相互转换:
    package com.yootk.demo;
    import java.util.Date;
    public class TestDemo {
        public static void main(String[] args) throws Exception {
            Date date = new Date();
            System.out.println(date); 		// 输出对象信息
            long cur = date.getTime();		// 将日期时间变成long型
            System.out.println(cur);
    	}
    }
    /*
    程序执行结果:
    	Sun Apr 04 10:44:15 CST 2021
    	1617504255560
    */
               

日期格式化:SimpleDateFormat

虽然使用java.util.Date类可以明确取得当前日期时间,但最终数据的显示格式不方便用户阅读。如果要对显示的日期时间进行格式转换,可以通过java.text.SimpleDateFormat类完成。

  • java.text.SimpleDateFormat类常用方法:
    public SimpleDateFormat(String pattern)					// 构造,传入日期时间标记实例化对象
    public final String format(Date date)					// 将日期格式化为字符串数据
    public Date parse(String source) throws ParseException	// 将字符串格式化为日期数据
               
  • 日期时间标记:年(yyyy)、月(MM)、日(dd)、时(HH)、分(mm)、秒(ss)、毫秒(SSS)
  • 将日期格式化显示:
    package com.yootk.demo;
    import java.text.SimpleDateFormat;
    import java.util.Date;
    public class TestDemo {
        public static void main(String[] args) throws Exception {
            Date date = new Date();					// 实例化Date类对象
            // 实例化SimpleDateFormat类对象,同时定义好要转换的目标字符串格式
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
            String str = sdf.format(date); 			// 将Date型变为String型
            System.out.println(str);
        }
    }
    // 程序执行结果:	2021-04-04 11:01:42.339
               
  • SimpleDateFormat可以自动处理错误的日期时间数,比如13月,会自动加一年,然后算成1月。
    package com.yootk.demo;
    import java.text.SimpleDateFormat;
    import java.util.Date;
    public class TestDemo {
        public static void main(String[] args) throws Exception {
            String str = "2005-15-57 77:95:22.111" ;	// 字符串由日期时间组成
            // 实例化SimpleDateFormat类对象,同时定义好要转换的目标字符串格式
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS") ;
            Date date = sdf.parse(str) ;				// 将字符串变为日期型数据
            System.out.println(date);
        }
    }
    // 程序执行结果:	Sat Apr 29 06:35:22 CST 2006
               
  • 关于数据类型转换:
    • Date与String类之间的转换依靠SimpleDateFormat类
    • String与8种基本数据类型的转换依靠包装类与String.valueOf()方法
    • longyuDate转换依靠Date类提供的构造以及getTime()方法

Calendar类

Calendar类可以取得单独的日期时间的时分秒日月年。

  • Calendar类中定义的常量与方法:
    【Java基础】Java反射机制、正则表达式、比较器、GC垃圾回收、StringBuffer类、对象克隆、日期处理
  • Calendar类是一个抽象类,所以不能直接实例化对象,其获得其实例化有两种方式:
    • 使用其子类GregorianCalendar类进行实例化
    • 使用Calendar类中的getInstance()方法获得本类的实例化对象。
  • 取得当前的日期时间:
    package com.yootk.demo;
    import java.util.Calendar;
    public class TestDemo {
        public static void main(String[] args) throws Exception {
            Calendar cal = Calendar.getInstance(); 					// 取得本类对象
            StringBuffer buf = new StringBuffer();					// 保存日期时间数据
            buf.append(cal.get(Calendar.YEAR)).append("-");			// 取得年数据
            buf.append(cal.get(Calendar.MONTH) + 1).append("-");	// 取得月数据,从0开始
            buf.append(cal.get(Calendar.DAY_OF_MONTH)).append(" ");	// 取得天数据
            buf.append(cal.get(Calendar.HOUR_OF_DAY)).append(":");	// 取得小时数据
            buf.append(cal.get(Calendar.MINUTE)).append(":");		// 取得分钟数据
            buf.append(cal.get(Calendar.SECOND));					// 取得秒数据
            System.out.println(buf);
        }
    }
    // 程序执行结果:	2021-4-4 11:17:49
               
    注意Calendar类取得的月份是从0开始的,所以月份需要+1

10.7 比较器

在Java中存在对象数组概念,可以利用比较器实现对象数组的比较操作。

Arrays类

在Java中,java.util.Arrays类中定义了所有的与数组有关的操作,如:二分查找、相等判断、数组填充等

  • Arrays类的常用方法
    public static boolean equals(int[] a, int[] b)		// 普通,判断两个数组是否相等
    public static void fill(int[] a, int val)			// 将指定内容填充到数组中
    public static void sort(int[] a)					// 数组排序
    public static int binarySearch(int[] a, int key)	// 使用二分查找法对排序后的数组进行检索
    public static String toString(int[] a)				// 输出数组信息
               
    以上方法被重载多次,所以不只适用于int型数组,还适用于其他数据类型的数组,除了8个基本数据类型数组,还适用于对象数组,不过该对象所属的类需要实现某些接口。
  • 二分查找法(binarySearch()):又被称为伴着查找法,在金乡数据查找时速度比较快。但要想使用二分查找法,则要求数组必须是有序的。其原理如图10-4所示:
    【Java基础】Java反射机制、正则表达式、比较器、GC垃圾回收、StringBuffer类、对象克隆、日期处理
  • 二分查找法(binarySearch())的基本思路是将n个元素的数组分成大致相等的两个部分。假设需要查找的数据类容为x,则取""数组[长度/2]"与x进行比较:
    • x = 数组[长度/2],则表示存在x,算法终止;
    • x < 数组[长度/2],则只在数组左半部分继续搜索x;
    • x > 数组[长度/2],则只在数组右半部分继续搜索x;
  • 实现二分查找:
    package com.yootk.demo;
    import java.util.Arrays;
    public class TestDemo {
        public static void main(String[] args) throws Exception {
            int data[] = new int[] { 1, 5, 6, 2, 3, 4, 9, 8, 7, 10 };
            java.util.Arrays.sort(data);						// 数组必须排序才可以查找
            System.out.println(Arrays.binarySearch(data, 9));	// 二分查找法
        }
    }	
               

比较器:Comparable

数组分为普通数组和对象数组,普通数组排序可以直接调用Arrays.sort()进行,但对象数组由于其每个元素其实存放的是地址数据,所以不能依据其大小关系来排序,但Arrays类中依然重载了一个sort()方法:public static void sort(Object[] a),此方法可以直接实现对象数组的排序。但使用此方法的前提是,对象所在的类一定要实现Comparable接口,否则就会出现ClassCastException异常。

  • Comparable接口定义如下
    public interface Comparable<T> {
        public int comparaTo(T o);
    }
               
    在Comparable接口中制定一了一个comparaTo()方法,返回值为int型,而用户覆写此方法时只需返回3种结果:1(>)、0(=)、-1(<)
  • 实现对象数组排序
    package com.yootk.demo;
    import java.util.Arrays;
    class Book implements Comparable<Book> {		// 实现比较器
        private String title ;
        private double price ;
        public Book(String title,double price) {
            this.title = title ;
            this.price = price ;
        }
        @Override
        public String toString() {
            return "书名:" + this.title + ",价格:" + this.price + "\n" ;
        }
        @Override
        public int compareTo(Book o) {				// Arrays.sort()会自动调用此方法比较
            if (this.price > o.price) {
               return 1 ; 
            } else if (this.price < o.price) {
               return -1 ;
            } else {
               return 0; 
            }
        }
    }
    public class TestDemo {
        public static void main(String[] args) throws Exception {
            Book books [] = new Book [] {
                     new Book("Java开发实战经典",79.8) ,
                     new Book("JavaWEB开发实战经典",69.8) ,
                     new Book("Oracle开发实战经典",99.8) ,
                     new Book("Android开发实战经典",89.8) 
            } ;
            Arrays.sort(books);						// 对象数组排序
            System.out.println(Arrays.toString(books));
        }
    }
               
  • 注意:对象数组要想使用Arrays.toString()方法,则需要在对象所属的类中覆写Object.toString()方法,否则直接使用Arrays.toString()方法返回的是对象信息。

数据结构——BinaryTree(二叉树)

树是一种比链表更加复杂的概念应用,其本质也属于动态数组对象,但是与链表不同的是,树的最大特征是可以针对数据进行排序。

  • 树的操作原理:选择第一个数据作为根节点,而后比根节点小的放在根节点的左子树(左节点),比根节点大的数据放在右子树(右节点),取得数据时按照中序遍历的方式取出(左->中->右)。但是如果要想实现这样的排列,则需要有一个数据的包装类Node,而且在此类中除了要保存数据外,还需要保存对应的左子树以及右子树节点对象,如图10-5所示:
    【Java基础】Java反射机制、正则表达式、比较器、GC垃圾回收、StringBuffer类、对象克隆、日期处理
  • 其实常用的数据结构已经被Java完整地实现好了,在本书第13章类集框架中会有详细的讲解,此处只是一个原理的简单分析。
  • 实现二叉树:
    • 定义出需要使用的数据类型,并且该类一定要实现Comparable接口,并覆写compareTo()方法
      class Book implements Comparable<Book> {		// 实现比较器
          private String title ;
          private double price ;
          public Book(String title,double price) {
              this.title = title ;
              this.price = price ;
          }
          @Override
          public String toString() {					// 覆写Object类的toString()方法
              return "书名:" + this.title + ",价格:" + this.price + "\n" ;
          }
          @Override
          public int compareTo(Book o) {				// 覆写Comparable接口的compareTo()方法
              if (this.price > o.price) {
                 return 1 ; 
              } else if (this.price < o.price) {
                 return -1 ;
              } else {
                 return 0; 
              }
          }
      }
                 
    • 定义二叉树,所有的数据结果都通过Node类的对象包装,同时为了排序需要,保存数据可以直接用Comparable接口类型,所有的类对象都必须强制转换为Comparable接口。
      @SuppressWarnings("rawtypes")
      class BinaryTree {
          private class Node {                                    // 定义内部类Node
              private final Comparable data;                      // 排序依据就是Comparable
              private Node left;                                  // 保存左节点
              private Node right;                                 // 保存右节点
              public Node(Comparable data) {                      // 定义Node构造方法
                  this.data = data;
              }
              @SuppressWarnings("unchecked")
              public void addNode(Node newNode) {
                  if (this.data.compareTo(newNode.data) > 0) {    // 比this节点小放左边
                      if (this.left == null) {                    // 左节点为null
                          this.left = newNode;                    // 保存到左节点
                      } else {
                          this.left.addNode(newNode);             // 接续递归判断
                      }
                  } else {                                        // 比this节点大或等于的放右边
                      if (this.right == null) {                   // 右节点为null
                          this.right = newNode;                   // 保存到右节点
                      } else {
                          this.right.addNode(newNode);            // 继续递归判断
                      }
                  }
              }
              public void toArrayNode() {                         // 将节点转换为对象数组
                  if (this.left != null) {                        // 表示有左节点
                      this.left.toArrayNode();                    // 继续递归判断
                  }
                  // 先判断左节点,再取出中间节点,再取得右节点。(中序遍历)
                  BinaryTree.this.retData[BinaryTree.this.foot++] = this.data;    
                  if (this.right != null) {                       // 表示有右节点
                      this.right.toArrayNode();                   // 继续递归判断
                  }
              }
          }
          //*******************以上为内部类************************//
          private Node root;                                      // 保存根节点
          private int count;                                      // 保存节点数
          private Object[] retData;                               // 保存返回的对象数组
          private int foot;                                       // 操作角标
      
          public void add(Object obj) {                           // 进行数据追加
              Comparable com = (Comparable) obj;                  // 向下转型为Comparable
              Node newNode = new Node(com);                       // 新建节点
              if (this.root == null) {
                  this.root = newNode;
              } else {
                  this.root.addNode(newNode);
              }
              this.count++;
          }
          public Object[] toArray() {                             // 将树转为数组形式返回
              if (this.root == null) {
                  return null;
              }
              this.foot = 0;
              this.retData = new Object[this.count];
              this.root.toArrayNode();
              return this.retData;
          }
      }
                 
    • 测试程序:
      public class TestDemo {
          public static void main(String[] args) throws Exception {
              BinaryTree bt = new BinaryTree();                       // 保存二叉树
              bt.add(new Book("Java开发实战经典",79.8));      // 新增节点
              bt.add(new Book("JavaWEB开发实战经典",69.8));
              bt.add(new Book("Oracle开发实战经典",99.8));
              bt.add(new Book("Android开发实战经典",89.8));
              Object[] obj = bt.toArray();                            // 将数据转为对象数组保存
              System.out.println(Arrays.toString(obj));               // 打印数据
          }
      }
      /*
      程序执行结果:
      	[书名:JavaWEB开发实战经典,价格:69.8, 
      	 书名:Java开发实战经典,价格:79.8, 
      	 书名:Android开发实战经典,价格:89.8, 
      	 书名:Oracle开发实战经典,价格:99.8]
      */
                 
    本程序实现了一个最基础的二叉树数据结构,整个程序的实现关键在于Node节点的比较判断。

挽救的比较器:Comparator

想使用Comparable比较器,就意味着得在对象的类定义时就必须考虑好排序的需求。但如果一个类在定义时没有实现Comparable接口,但又想实现对象数组的排序怎么办?为此,在Java中又提供了一个比较器:Comparator接口(挽救的比较器)。

  • Comparator接口定义:
    @FunctionalInterface
    public interface Comparator<T> {
        public int compate(T o1, T o2);
        public boolean equlas(Object obj);
    }
               
    请注意:Comparator接口使用了@FunctionalInterface注解进行声明,说明其为一个函数式接口,前面我们学函数式接口时说过:函数式接口只能有一个抽象方法,但这里为什么可以定义两个?其实是因为equlas()方法是Object类里的,不算在Comparator接口中。
  • 如果想要用Comparator接口实现对象数组的排序操作,需要使用这个数组排序方法:
  • 使用Comparator接口实现对象数组的排序:
    package com.yootk.demo;
    class Book { 
        private String title ;
        private double price ;
        public Book() {}
        public Book(String title,double price) {
            this.title = title ;
            this.price = price ;
        }
        @Override
        public String toString() {
            return "书名:" + this.title + ",价格:" + this.price + "\n" ;
        }
        public double getPrice() {
            return price;
        }
    }
    class BookComparator implements java.util.Comparator<Book> {
        @Override
        public int compare(Book o1, Book o2) {
            if (o1.getPrice() > o2.getPrice()) {
               return 1 ; 
            } else if (o1.getPrice() < o2.getPrice()) {
               return -1 ;
            } else {
               return 0; 
            }
        }
    }
    public class TestDemo {
        public static void main(String[] args) throws Exception {
            Book[] books = new Book[] {
                new Book("Java开发实战经典",79.8);
                new Book("JavaWEB开发实战经典",69.8);
                new Book("Oracle开发实战经典",99.8);
                new Book("Android开发实战经典",89.8);
            };
            java.util.Arrays.sort(books, new BookComparator());
            System.out.println(java.util.Arrays.toString());
        }
    }
               
  • 由于Comparator接口为函数式接口,所以用户也可以使用Lambda表达式来实现比较规则定义
    public class TestDemo {
        public static void main(String[] args) throws Exception {
            Book[] books = new Book[] {
                new Book("Java开发实战经典",79.8);
                new Book("JavaWEB开发实战经典",69.8);
                new Book("Oracle开发实战经典",99.8);
                new Book("Android开发实战经典",89.8);
            };
            java.util.Arrays.sort(books, (o1, o2) -> {				// Lambda表达式
                if (o1.getPrice() > o2.getPrice()) {
                    return 1 ;
                } else if (o1.getPrice() < o2.getPrice()) {
                    return -1 ;
                } else {
                    return 0;
                }
            });
            System.out.println(java.util.Arrays.toString());
        }
    }
               
  • Comparable和Comparator的区别:
    • java.lang.Comparable:是一个需要在类定义时就实现好的接口,实现该接口时覆写public int compareTo()方法
    • java.util.Comparator:是一个挽救比较器,无需再类定义时实现,实现时应覆写public int compare()方法。

10.8 正则表达式

正则表达式(Regular Expression,在代码中常简写为regex、regexp或RE)是从JDK1.4引入到Java中的。正则表达式在本质上是一种字符串操作的语法规则,利用此语法规可以更加灵活地实现字符串的匹配、拆分、替换等操作。

正则标记

  • 所有的正则表达式支持的类都定义在java.util.regex包里面,包中定义如下主要的类:
    • Pattern类:主要定义要使用的表达式对象;
    • Matcher类:用于进行正则标记与指定类容的匹配操作;
  • 所有可以使用的正则标记都在java.util.regex.Pattern类的说明文档中定义,常用的标记有6种:
  • 单个字符:
    • 字符:表示由一位字符组成;
    • \\:表示转义字符 “\”;
    • \t:表示一个“\t”符号(Tab键);
    • \n:匹配换行(\n)符号;
  • 字符集:
    • [abc]:表示可能是字符a、字符b、字符c中的任意一位;
    • [^abc]:表示不是字符a、b. c中的任意一位;
    • [a-z]:表示所有小写字母
    • [a-zA-Z]:表示任意的一位字母,不区分大小写;
    • [0-9]:表示任意的一-位数字;
  • 简化的字符集表达式:
    • .:表示任意的一位字符;

      \d:等价于“[0-9]”, 属于简化写法;

      \D:等价于“[^0-9]”, 属于简化写法;

      \s:表示任意的空白字符,例如:“\t” “\n";

      \S:表示任意的非空白字符;

      \w:等价于“[a-zA-Z_ 0-9]”, 表示由任意的字母、数字、_组成;

      \W:等价于“[^a-zA-Z _0-9]”, 表示不是由任意的字母、数字、 _组成;

  • 边界匹配

    ^:正则的开始;

    $:正则的结束;

  • 数量表达
    • 正则?:表示此正则可以出现0次或1次;
    • 正则+:表示此正则可以出现1次或1次以上;
    • 正则*:表示此正则可以出现0次、1次或多次;
    • 正则{n}:表示此正则正好出现n次;
    • 正则{n,}:表示此正则出现n次以上(包含n次);
    • 正则{n,m};表示此正则出现n~ m次;
  • 逻辑运算
    • 正则1正则2:正则1判断完成后继续判断正则2;
    • 正则1 |正则2:正则1或者是正则2有一-组满足即可;
    • (正则):将多个正则作为一-组,可以为这一-组单独设置出现的次数。

String类对正则的支持

虽然Java本身提供的正则支持类都在java.util.regex包中,但从实际使用来看,很少会利用这个包中的正则操作。大部分的情况下都会考虑使用java.lang.String类中提供的方法来直接简化正则的操作。

  • String类中与正则有关的5个操作方法:
    public boolean matches(String regex)							// 普通,正则验证,使用指定的字符串判断其是否符合给出的正则表达式regex结构
    public String replaceAll(String regex, String replacement)		// 普通,将满足正则表达式regex的内容全部替换为新内容replacement
    public String replaceFirst(String regex, String replacement)	// 普通,将满足正则表达式regex的首个内容替换为新内容replacement
    public String[] split(String regex)								// 普通,按照指定的正则表达式regex进行字符串的全部拆分
    public String[] split(String regex, int limit)					// 普通,按照指定的正则表达式regex将字符串拆分为limit部分
               
  • 实现字符串替换:
    package com.yootk.demo;
    public class TestDemo {
        public static void main(String[] args) throws Exception {
            String str = "hello*)(*()yootk(*#mldn*";
            String regex = "[^a-z]"; 								// 此处编写正则表达式
            System.out.println(str.replaceAll(regex, ""));			// 字符串替换
        }
    }
    // 程序执行结果:	helloyootkmldn
               
    正则表达式regex = “[^a-z]”,表示匹配所有非小写字母的字符,再执行 str.replaceAll(regex, “”) ,会把字符串str中所有的非小写字母的字符全部用”“替代(删除)。
  • 字符串拆分:
    package com.yootk.demo;
    public class TestDemo {
        public static void main(String[] args) throws Exception {
            String str = "yootk9mldnyo8798o5555tk";
            String regex = "\\d+"; 							// \d:[0-9]  ,  +:一次及以上
            String result[] = str.split(regex);				// 以数字进行拆分
            for (int x = 0; x < result.length; x++) {
                System.out.print(result[x] + "  ");
            }
        }
    }
    // 程序执行结果:	yootk  mldnyo  o  tk  
               
    注意:这里的“\\d+”为什么有两个“\”呢?因为“\”表示转义符,两个“\”表示不需要转义。
  • 判断是否为IP地址(IPV4)
    package com.yootk.demo;
    public class TestDemo {
        public static void main(String[] args) throws Exception {
            String str = "192.168.1.1";
            String regex = "\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}";
            System.out.println(str.matches(regex));
        }
    }
               
    IPV4地址是由4组数字数据和3个“.”组成,所以需要4组"\\d{1, 3}"来判断数字,和3组“\\.”来判断“.”。
  • 为什么“.”符号需要使用“\\.”来判断?

    因为“.”本身就代表一个正则标记,所以需要在“.”前面使用转义符“\”,而转义符在正则标记中使用“\\”来表示。所以使用“\\.”来表示“.”。

  • 简化正则表达式:
    String regex = "\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}";			// 简化为:
    String regex = "(\\d{1,3}\\.){3}\\d{1,3}";
               
  • 判断是否为Email,要求:用户名要求由字母、数字、“_”、“.”组成,必须以字母开头,结尾必须为字母或数字,长度为2-30最后域名只能为.com、.cn、.net、.com.cn、.net.cn、.edu、.goc、.org其中之一。完整Email组成正则分析如图10-8所示。
    【Java基础】Java反射机制、正则表达式、比较器、GC垃圾回收、StringBuffer类、对象克隆、日期处理
    package com.yootk.demo;
    public class TestDemo {
        public static void main(String[] args) {
            String str = "[email protected]";
            String regex = "[a-zA-Z][a-zA-z0-9_\\.]{0,28}[a-zA-Z]@\\w+\\.(com|cn|net|com\\.cn|net\\.cn|edu|goc|org)";
            System.out.println(str.matches(regex));
        }
    }
               

10.9 反射机制

反射是Java中最为重要的特性,几乎所有的开发框架以及应用技术中都是基于反射技术的应用。

认识反射

在正常的操作过程中,一定是先确定使用的类,再利用关键字new产生实例化对象后使用。但是也可以通过Object类中的getClass()方法获得对象所在的类的信息。

  • java.lang.Object.getClass()方法定义:
  • 反射初步操作:
    package com.yootk.demo;
    import java.util.Date; 								// 导入所需要的类
    public class TestDemo {
        public static void main(String[] args) throws Exception {
            Date date = new Date(); 					// 产生实例化对象
            System.out.println(date.getClass());		// 直接反射输出
        }
    }
    // 程序执行结果:	class java.util.Date
               

Class类对象实例化

当我们使用getClass()方法时,返回值类型为java.lang.Class,这是反射操作的源头类。而Class类有3种实例化的方式:

  • 方式一:调用Object类中的getClass()方法,但这种方法必须要有Object对象才能使用。
  • 方式二:使用 “ 类名.class ” 获得Class类实例化对象,这种方法不需要对象只需要类名就可以使用
    package com.yootk.demo;
    public class TestDemo {
        public static void main(String[] args) throws Exception {
            Class<?> cls = java.util.Date.class;	// 通过类名取得Class对象
            System.out.println(cls.getName());		// 输出对象所在类的名称
        }
    }
    // 程序执行结果:	java.util.Date
               
    Class类中定义有 “ public String getName() ”方法获得类的完整名称的String字符串形式
  • 方式三:调用Class类提供的forName()方法:
    public static Class<?> forName(String className) throws ClassNotFoundException
               
    package com.yootk.demo;
    public class TestDemo {
        public static void main(String[] args) throws Exception {
            Class<?> cls = Class.forName("java.util.Date");		// 通过String字符串类型的类的完整名称,取得Class对象
            System.out.println(cls.getName());					// 输出对象所在类的名称
        }
    }
    // 程序执行结果:	java.util.Date
               
  • 几乎所有开发框架都会用到以上的3种实例化Class类对象的方式。

反射实例化对象

掌握了Class类对象实例化的三种方式,就可以利用Class类来进行类的反射控制了。

  • Class类中提供有以下10个常用方法:
    public static Class<?> forName(String className) throws ClassNotFoundException	// 通过字符串设置的类名称实例化Class类对象
    public Class<?>[] getInterface()												// 取得类实现的所有接口
    public String getName()															// 取得反射操作类的全名
    public String getSimpleName()													// 取得反射操作类名,不包括包名
    public Package getPackage()														// 取得反射操作类所在的包
    public Class<? super T> getSuperclass()											// 取得反射操作类的父类
    public boolean isEnum()															// 判断反射操作类是否为枚举
    public boolean isInterface()													// 判断反射操作类是否为接口
    public boolean isArray()														// 判断反射操作类是否为数组
    public T newInstance() throws InstantionException, IllegalAccessException		// 反射实例化对象
               
  • 在以上Class类的方法中,最为重要的就是newInstance()方法,通过此方法就可以利用反射实现反射操作类的实例化对象。也就是说不用关键字new就可以实现实例化操作。
  • 使用newInstance()方法实例化对象时默认调用无参构造方法,所以想用Class类中的newInstance()方法反射实例化对象,则类中一定要提供一个无参构造方法。
  • 利用反射实例化对象:
    package com.yootk.demo;
    class Book {
        public Book() {
            System.out.println("********** Book类的无参构造方法 ***********");
        }
        @Override
        public String toString() {
            return "《名师讲坛——Java开发实战经典》";
        }
    }
    public class TestDemo {
        public static void main(String[] args) throws Exception {
            Class<?> cls = Class.forName("com.yootk.demo.Book");		// 设置要操作对象的类名称
            // 反射实例化后的对象返回的结果都是Object类型,如果有需要可以对其进行向下转型强制变成子类对象
            Object obj = cls.newInstance(); 							// 相当于使用new调用无参构造
            Book book = (Book) obj;										// 向下转型
            System.out.println(book);
        }
    }
    /*
    程序执行结果:
    	********** Book类的无参构造方法 ***********
    	《名师讲坛——Java开发实战经典》
    */
               
    使用newInstance()方法反射实例化对象的返回结果都是Object类型,如果有需要可以对其进行向下转型强制变成子类对象再进行操作。

利用反射实现工厂设计模式

  • 反射机制实例化对象的意义:
    • 使用关键字new实例化需要明确地指出类的构造方法,所以new是造成耦合的最大元凶,而要解决代码耦合问题,首先就要解决new实例化对象的操作。
    • 在"4.7接口"中,我们介绍了“工厂设计模式”,但当时的工厂设计模式虽然解决了一定的代码耦合问题,即增加新的接口子类可以不用更改客户端代码;
    • 但还是存在一个问题:就是增加新的接口子类还是需要修改工厂类。这就是使用关键字new而带来的问题,而现在可以利用反射解决此问题。
  • 利用反射实现工厂设计模式:
    package com.yootk.test;
    interface Fruit {
        public void eat() ;
    }
    class Apple implements Fruit {
        @Override
        public void eat() {
            System.out.println("** 吃苹果!");
        }
    }
    class Orange implements Fruit {
        @Override
        public void eat() {
            System.out.println("** 吃橘子!");
        }
    }
    class Factory {
        public static Fruit getInstance(String className) {
            Fruit f = null;
            try {														// 反射实例化,子类对象可以使用Fruit接收
                f = (Fruit) Class.forName(className).newInstance();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return f;
        }
    }
    public class TestFactory {
        public static void main(String[] args) {	
            Fruit fa = Factory.getInstance("com.yootk.test.Apple") ;  	// 直接传递类名称
            Fruit fb = Factory.getInstance("com.yootk.test.Orange") ;	// 直接传递类名称
            fa.eat();
            fb.eat();
        }
    }
    /*
    程序执行结果:
    	** 吃苹果!
    	** 吃橘子!
    */
               
    本程序在工厂类中使用了反射机制,这样只需要传递完整的类名就可以取得实例化对象了。在实际开发中,如果将以上工厂设计模式再结合一些配置文件(如:XML格式的文件),就可以利用配置文件来动图定义项目中所需要的操作类,此时的程序将变得非常灵活。

反射调用构造

利用Class类的newInstance()方法可以实现反射实例化对象的操作,但其还是有一定限制,就是类中一定要提供无参构造方法。所以当类中只提供了有参构造时,就必须通过 java.lang.reflect.ConStructor类来实现对象的反射实例化操作。

  • Class类中定义了取得反射操作类的构造方法的操作:
    public Constructor<?>[] getConstructors() throws SecurityException		// 普通,取得全部构造方法
    public Constructor<T> getConstructor(Class<?> ... parameterTypes) throws NoSuchMethodException, SecurityException	//取得指定参数类型的构造方法
               

这两个方法的返回值类型为java.lang.reflect.Constructor类

  • java.lang.reflect.Constructor类常用方法:
    public Class<?>[] getExceptionTypes()					// 普通,返回构造方法上所有抛出异常的类型
    public int getModifiers()								// 取得构造方法的修饰符
    public String getName()									// 取得构造方法的名字
    public int getParameterCount()							// 取得构造方法中参数的个数
    public Class<?> getParameterTypes()						// 取得构造方法中的参数类型
    public T newInstance(Object... initargs) throws InstantionException, IllegalAccessException, IllegalArgumentException, InvocationTargetException								// 调用指定参数的构造实例化对象
               
    从上可知,Constructor类中也存在newInstance()方法,并且此方法定义时使用了可变参数形式,这样先通过Class类找到指定参数类型的构造方法,再利用Constructor类的newInstance()方法传入实例化对象所需的参数就可以实现指定参数的构造方法调用了。
  • 为什么取得修饰符的方法返回值为int型?

    因为所有修饰符都是一个数字,修饰符的组成就是一个数字的加法操作。假设: public 使用1表示,final使用16表示,static使用8表示,如果是public final那么就使用17表示(1 + 16),而如果是public static final 那么就使用25表示(1 +8+ 16),所以所有的修饰符本质上都是数字的加法操作。

    在java.lang.reflect.Modifer类中明确地定义了各个修饰符对应的常量操作,同时也提供了将数字转换为指定修饰符的方法:“public static String toString(int mod)"

  • 明确调用类中的有参构造:
    package com.yootk.demo;
    import java.lang.reflect.Constructor;
    class Book {
        private String title ;
        private double price ;
        public Book(String title, double price) {
            this.title = title ;
            this.price = price ;
        }
        @Override
        public String toString() {
            return "图书名称:" + this.title + ",价格:" + this.price ;
        }
    }
    public class TestDemo {
        public static void main(String[] args) throws Exception {
            Class<?> cls = Class.forName("com.yootk.demo.Book") ;			// 获得Class类实例化对象
            // 明确地找到Book类中两个参数的构造,第一个参数类型是String,第二个是double
            Constructor<?> con = cls.getConstructor(String.class,double.class) ;	//使用类名.class来获取该类的Class对象
            Object obj = con.newInstance("Java开发实战经典",79.8) ;			// 实例化对象,传递参数内容
            System.out.println(obj);
        }
    }
               
    首先利用Class.forName()方法获得Class对象;再利用Class对象调用getConstructor()取得Book类的对应参数的构造(返回Constructor类对象);再利用Constructor类中的 newInstance()方法传递指定数据,就可以调用有参构造进行对象的反射实例化。

反射调用方法

以往都是使用“对象.方法()”的形式进行方法调用,而现在可以利用反射机制实现类方法的操作。

  • 使用Class类取得普通方法:
    public Method[] getMethods() throws SecurityException		// 普通,取得类中全部的方法
    public Method getMethod(String name, Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException	// 普通,取得类中指定方法名称与参数类型的方法
               
    上述两种获得类普通方法的方法的返回值都是都是java.lang.reflect.Method类对象。
  • java.lang.reflect.Method类常用方法:
    public int getModifiers()								// 普通,取得方法的修饰符
    public Class<?> getReturnType()							// 普通,取得方法的返回值类型
    public int getParameterCount()							// 普通,取得方法中定义的参数数量
    public Class<?>[] getParameterTypes()					// 普通,取得方法中定义的所有参数类型
    public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException								  // 普通,反射调用方法并且传递执行方法所需要的参数数据
    public Class<?>[] getExceptionTypes()					// 普通,取得方式抛出的异常类型
               
    上面最重要的方法为invoke(),此方法是实现方法反射调用的核心操作。
  • 反射调用普通方法也是需要实例化对象的,invoke()方法中,接收的第一个参数就是类的实例化对象,但要注意的是,此时的类型使用的是Object,也就是用反射实现的方法调用,不需要具体的对象类型,这一点比直接使用对象调用方法更加灵活。且正好Class类中的newInstance()方法反射的实例化对象就是Object类型。
  • 使用反射调用普通方法:
    package com.yootk.demo;
    import java.lang.reflect.Method;
    class Book {
        private String title ;
        public void setTitle(String title) {
            this.title = title;
        }
        public String getTitle() {
            return title;
        }
    }
    public class TestDemo {
        public static void main(String[] args) throws Exception {
            String fieldName = "title" ;							// 要操作的成员名称
            Class<?> cls = Class.forName("com.yootk.demo.Book") ;	// 取得要操作类的反射对象
            Object obj = cls.newInstance() ;						// 必须实例化对象
            // 取得类中的setTitle()方法,由于title需要首字母大写,所以调用initcap()处理,参数类型为String
            Method setMet = cls.getMethod("set" + initcap(fieldName), String.class) ;
            // 取得类中的getTitle()方法,本方法不接收参数并且没有返回值类型声明
            Method getMet = cls.getMethod("get" + initcap(fieldName)) ;
            setMet.invoke(obj, "Java开发实战经典") ; 					// 等价于:Book类对象.setTitle("Java开发实战经典")
            System.out.println(getMet.invoke(obj));					// 等价于:Book类对象.getTitle()
        }
        public static String initcap(String str) {					// 首字母大写操作
            return str.substring(0, 1).toUpperCase() + str.substring(1) ;
        }
    }
    // 程序执行结果:	Java开发实战经典
               
    本程序首先利用Class.forName()方法获取Book类对应的Class类对象;再使用Class类中的newInstance()方法获得Book类的实例化对象,使用Object类接收;再使用Class类中的getMethod()方法获得Book类中的普通方法,以Method类对象形式保存;再通过Method类中的invoke()方法实现方法的反射调用。

反射调用成员

除了构造方法、普通方法外,类中最重要的组成就是成员(变量和常量)。

  • 使用Class类取得成员的操作:
    public Field[] getDeclaredFields() throws SecurityException		// 普通,取得本类定义的全部成员
    public Field getDeclaredField(String name) throws NoSuchFieldException, SecurityException	// 普通,取得本类指定名称的成员
    public Fidld[] getFields() throws SecurityException				// 普通,取得本类继承父类的全部成员
    public Field getField(String name) throws NoSuchFieldException, SecurityException			// 普通,取得本类继承父类中指定名称的成员
               
    以上四种方法的返回值类型都为:java.lang.reflect.Field,此类可以描述类中的成员信息。
  • java.lang.reflect.Field类的常用方法:
    public Class<?> getType()					// 普通,取得该成员的数据类型
    
    public void set(Object obj, Object value) throws IllegalAccessException, IllegalArgumentException	// 普通,取得指定对象中的成员的内容,相当于直接调用成员
    public Object get(Object obj) throws IllegalAccessException, IllegalArgumentException	// 普通,设置指定对象中的成员内容,相当于直接利用对象调用成员设置内容
               
    实际上在Field类中还定义了许多例如setInt()、setDouble()、getChar()等方法,可以直接设置这些方法或方法的具体类型。
  • 对于被封装的成员,不能直接使用get和set方法操作,需要先对它进行取消封装。Field类在定义时继承了AccessibleObject类,而在AccessibleObject类中定义了一个setAccessible()方法,该方法的作用就是用来取消成员的封装属性。java.lang.reflect.AccessibleObject.setAccessible()方法定义如下
    public void setAccessible(boolean flag) throws SecurityException
               
    setAccessible()方法参数flag的值为true时,表示反射的对象在使用该成员时应禁止Java语言访问检查,即取消封装。
  • AccessibleObject类与Constructor、Method、Field类的关系:
    【Java基础】Java反射机制、正则表达式、比较器、GC垃圾回收、StringBuffer类、对象克隆、日期处理
    由上图可知:Constructor、Method、Field三个类都是AccessibleObject类的子类,也就是说这三个类的对象都可以使用setAccessible()方法取消封装。
  • 跟利用反射调用方法一样,反射调用成员同样需要实例化的对象。
  • 利用反射直接操作私有成员:
    package com.yootk.demo;
    import java.lang.reflect.Field;
    class Book {										// 为了演示,所以使用非标准简单Java类
        private String title ;							// 私有属性,并没有定义setter、getter方法
    }
    public class TestDemo {
        public static void main(String[] args) throws Exception {
            Class<?> cls = Class.forName("com.yootk.demo.Book");	// 取得反射对象
            Object obj = cls.newInstance(); 						// 必须给出实例化对象
            Field titleField = cls.getDeclaredField("title");		// 取得类中的title属性
            titleField.setAccessible(true);							// 取消封装
            titleField.set(obj, "Java开发实战经典");					// 相当于:Book类对象.title = "数据"
            System.out.println(titleField.get(obj)); 				// 相当于:Book类对象.title
       }
    }
               
  • 虽然可以通过反射直接调用成员,但这样的操作还是不标准的,尽量还是使用setter和getter方法操作成员。

反射机制总结

Java反射机制的核心是在程序运行时动态加载类并获取类的详细信息,从而操作类或对象的属性和方法。

  • 反射机制中主要用到了4个类
    • java.lang.Class类
    • java.lang.reflect.ConStructor类
    • java.lang.reflect.Method类
    • java.lang.reflect.Field类
  • Class类对象实例化的3种方式:
    • 方式一:调用Object类中的getClass()方法,但这种方法必须要有Object对象才能使用。
    • 方式二:使用 “ 类名.class ” 获得Class类实例化对象,这种方法不需要对象只需要类名就可以使用
    • 调用Class类提供的forName()方法:
      public static Class<?> forName(String className) throws ClassNotFoundException
                 
  • java.lang.Class类最重要的4类用法:
    • 反射实例化对象,返回值为Object类型:
    • 取得类中的构造方法,返回值为Constructor类型:
      public Constructor<?>[] getConstructors() throws SecurityException				// 普通,取得全部构造方法
      public Constructor<T> getConstructor(Class<?> ... parameterTypes) throws NoSuchMethodException, SecurityException	//取得指定参数类型的构造方法
                 
    • 取得类中普通方法,返回值为Method类型:
      public Method[] getMethods() throws SecurityException		// 普通,取得类中全部的方法
      public Method getMethod(String name, Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException	// 普通,取得类中指定方法名称与参数类型的方法
                 
    • 取得类中成员,返回值为Field类型:
      public Field[] getDeclaredFields() throws SecurityException		// 普通,取得本类定义的全部成员
      public Field getDeclaredField(String name) throws NoSuchFieldException, SecurityException	// 普通,取得本类指定名称的成员
      public Fidld[] getFields() throws SecurityException				// 普通,取得本类继承父类的全部成员
      public Field getField(String name) throws NoSuchFieldException, SecurityException			// 普通,取得本类继承父类中指定名称的成员
                 
  • java.lang.reflect.ConStructor类最重要的方法:
  • java.lang.reflect.Method类最重要的方法:
  • java.lang.reflect.Field类最重要的方法:
    public void set(Object obj, Object value) throws IllegalAccessException, IllegalArgumentException	// 普通,取得指定对象中的成员的内容,相当于直接调用成员
    public Object get(Object obj) throws IllegalAccessException, IllegalArgumentException	// 普通,设置指定对象中的成员内容,相当于直接利用对象调用成员设置内容
               

本章小结

  • 当一个字符串内容需要频繁修改时,使用StringBuffer可以提升操作性能,因为StringBuffer的内容是可以改变的,而String的内容是不可以改变的。
  • StringBuffer类中提供了大量的字符串操作方法:增加、替换、插入等。
  • Runtime表示运行时,在一个JVM中只存在一个 Runtime,所以如果要取得Runtime类的对象,可以直接使用 Runtime类中提供的静态方法:getRuntime()。
  • System类是系统类,可以取得系统的相关信息,使用System.gc()方法可以强制性地进行垃圾的收集操作,调用此方法实际上就是调用了Runtime类中的gc()方法。
  • 使用 Date 类可以方便地取得时间,但取得的时间格式不符合地域的风格,所以可以使用SimpleDateFormat类进行日期的格式化操作。
  • 处理大数字可以使用:BigInteger 、BigDecimal,当需要精确小数点操作位数时使用BigDecimal类即可。
  • 通过Random类可以取得指定范围的随机数字。
  • 如果一个类的对象要想被克隆,则此对象所在的类必须实现Cloneable接口。
  • 要想对一组对象进行排序,则必须使用比较器,比较器接口Comparable中定义了一个compareTo()的比较方法,用来设置比较规则。
  • 正则表达式是在开发中最常使用的一种验证方法,在 JDK 1.4之后,String 类中的replaceAll()、split()、matches()方法都支持正则表达式。
  • Class类是反射机制操作的源头,Class类的对象有3种实例化方式:通过Object类中的getClass()方法;通过“类.class”的形式;通过Class.forName()方法,此种方式最为常用。
  • 可以通过 Class类中的newInstance()方法进行对象的实例化操作,但是要求类中必须存在无参构造方法,如果类中没有无参构造,则必须使用Constructor类完成对象的实例化操作。