【高级篇】Java的回顾总结

基础篇内容的补充:面向过程(POP)与面向对象(OOP):

  • 二者都是一种思想,面向对象是相对于面向过程而言的。
  • 面向过程,强调的是功能行为,以函数为最小单位,考虑怎么做。
  • 面向对象,将功能封装进对象,强调具备了功能的对象,以类/对象为最小单位,考虑谁来做。
  • 面向对象更加强调运用人类在日常的思维逻辑中采用的思想方法与原则,如抽象、分类、继承、聚合、多态等

1. Java常用类

1.1 String、StringBuffer、StringBuilder

1.1.1 String

①. String的特性

  • Java程序中的所有字符串字面值(如"abc")都作为String类的实例实现
  • String是一个final类,代表不可变的字符序列
  • 字符串是常量,用双引号引起来表示。它们的值在创建之后不能更改
  • String对象的字符内容是存储在一个字符数组value[ ]中的

②. 创建String对象的几种方式以及内存解析

// 字面量的方式
String str = "hello";
// 本质上this.value = new char[0];
String s1 = new String();
//this.value = original.value;
String s2 = new String(String original);
//this.value = Arrays.copyOf(value, value.length);
String s3 = new String(char[] a);
String s4 = new String(char[] a,int startIndex,int count);

总结:字面量的方式和new的方式的区别

字符串对象是如何存储的

③. 通过内存解析得出的结论

  • 常量与常量的拼接结果在常量池。且常量池中不会存在相同内容的常量。
  • 只要其中有一个是变量,结果就在堆中
  • 如果拼接的结果调用intern()方法,返回值就在常量池中

④. String的常用方法

int length():返回字符串的长度:return value.length
char charAt(int index):返回某索引处的字符return value[index]
boolean isEmpty():判断是否是空字符串:return value.length == 0
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):返回指定子字符串在此字符串中第一次出现处的索引
int indexOf(String str, int fromIndex):返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始
int lastIndexOf(String str):返回指定子字符串在此字符串中最右边出现处的索引
int lastIndexOf(String str, int fromIndex):返回指定子字符串在此字符串中最后一次出现处的索引,从指定的索引开始反向搜索
注:indexOf和lastIndexOf方法如果未找到都是返回-1
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个,如果超过了,剩下的全部都放到最后一个元素中

⑤. String与字符数组(char [ ])之间的转换

  • 字符数组 → 字符串
    • String类的构造器:String(char[ ]) 和 String(char[ ],int offset,int length)分别用字符数组中的全部字符和部分字符创建字符串对象
  • 字符串 → 字符数组
    • public char[ ] toCharArray():将字符串中的全部字符存放在一个字符数组中的方法。
    • public void getChars(int srcBegin, int srcEnd, char[ ] dst, int dstBegin):提供了将指定索引范围内的字符串存放到数组中的方法。

⑥. String与字节数组(byte [ ])之间的转换

  • 字节数组 → 字符串
    • String(byte[]):通过使用平台的默认字符集解码指定的 byte 数组,构造一个新的 String。
    • String(byte[],int offset,int length):用指定的字节数组的一部分,即从数组起始位置offset开始取length个字节构造一个字符串对象。
  • 字符串 → 字节数组
    • public byte[] getBytes():使用平台的默认字符集将此 String 编码为byte 序列,并将结果存储到一个新的 byte 数组中。
    • public byte[] getBytes(String charsetName):使用指定的字符集将此 String 编码到 byte 序列,并将结果存储到新的 byte 数组。

1.1.2 StringBuffer

①. StringBuffer的特性

  • java.lang.StringBuffer代表可变的字符序列,JDK1.0中声明,可以对字符串内容进行增删此时不会产生新的对象。
  • 很多方法与String相同。
  • 作为参数传递时,方法内部可以改变值

②. StringBuffer对象的创建以及扩容机制

StringBuffer类不同于String,其对象必须使用构造器生成。有三个构造器:

  • StringBuffer():初始容量为16的字符串缓冲区
  • StringBuffer(int size):构造指定容量的字符串缓冲区
  • StringBuffer(String str):将内容初始化为指定字符串内容

③. StringBuffer类的常用方法

当append和insert时,如果原来value数组长度不够,可扩容

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():把当前字符序列逆转

④. StringBuffer的扩容问题

如果要添加的数据底层数组盛不下了,就需要扩容底层的数组char[] value。(创建一个新数组,把原有数组的内容复制进去)默认情况下,扩容为原来容量的2倍+2,同时将原有数组中的元素复制到新的数组中。

1.1.3 StringBuilder

1.1.4 String与StringBuffer、StringBuilder之间的转换

  • String → StringBuilder、StringBuffer:调用StringBuilder、StringBuffer的构造器
  • StringBuilder、StringBuffer → String:
    • ①.调用String的构造器
    • ②.使用StringBuilder、StringBuffer的toString()方法

1.2 JDK8之前的日期时间API

计算世界时间的主要标准有:

  • 协调的通用时间:UTC(Coordinated Universal Time)
  • 格林威治标准时间:GMT(Greenwich Mean Time)
  • 中央标准时间:CST(Central Standard Time)

1.2.1 java.lang.System类

  • System类提供的public static long currentTimeMillis()用来返回当前时间与1970年1月1日0时0分0秒之间以毫秒为单位的时间差。此方法适于计算时间差。
long time = System.currentTimeMillis();

1.2.2 java.util.Date类:表示特定的瞬间,精确到毫秒

①. 构造器

  • Date():使用无参构造器创建的对象可以获取本地当前时间
  • Date(long date):创建指定毫秒数的Date对象

②. 常用方法

  • getTime():返回自1970年1月1日 00:00:00GMT 以来此 Date 对象表示的毫秒数(时间戳)。
  • toString():显示当前的年月日时分秒。把此 Date 对象转换为以下形式的String:dow mon dd hh:mm:ss zzz yyyy 其中:dow是一周中的某一天(Sun, Mon, Tue,Wed, Thu, Fri, Sat),zzz是时间标准。

③. 子类java.sql.Date:对应着数据库中的日期类型的变量

  • sql.Date → util.Date:直接赋值,多态
  • util.Date → sql.Date:强转或者util.Date.getTime获取时间戳,然后使用sql.Date的构造器

1.2.3 java.text.SimpleDateFormat类:格式化和解析日期的具体类

①. 格式化:日期 → 文本

  • SimpleDateFormat():默认的模式和语言环境创建对象
  • public SimpleDateFormat(String pattern):该构造方法可以用参数pattern指定的格式创建一个对象,该对象调用:public String format(Date date)方法格式化时间对象date
SimpleDateFormat sdf = new SimpleDateFormat();
Date date = new Date();
// 格式化date
String format = sdf.format(date);

②. 解析:文本 → 日期

  • public Date parse(String source):从给定字符串的开始解析文本,以生成一个日期
SimpleDateFormat sdf = new SimpleDateFormat();
Date date = new Date();
// 格式化date
String format = sdf.format(date);
// 解析format
Date date1 = sdf.parse(format);

1.2.4 java.util.Calendar(日历)类

  • Calendar是一个抽象基类,主要用于完成日期字段之间相互操作的功能。
  • 获取Calendar实例的方法
    • 使用Calendar.getInstance()方法
    • 调用它的子类GregorianCalendar的构造器。
  • 一个Calendar的实例是系统时间的抽象表示,通过get(int field)方法来取得想要的时间信息。比如YEAR、MONTH、DAY_OF_WEEK、HOUR_OF_DAY 、MINUTE、SECOND
    • public void set(int field,int value)
    • public void add(int field,int amount)
    • public final Date getTime()
    • public final void setTime(Date date)
  • 注意:
    • 获取月份时:一月是0,二月是1,以此类推,12月是11
    • 获取星期时:周日是1,周二是2 , 。。。。周六是7
Calendar calendar = Calendar.getInstance();
// 从一个 Calendar 对象中获取 Date 对象
Date date = calendar.getTime();
// 使用给定的 Date 设置此 Calendar 的时间
date = new Date(234234235235L);
calendar.setTime(date);
calendar.set(Calendar.DAY_OF_MONTH, 8);
System.out.println("当前时间日设置为8后,时间是:" + calendar.getTime());
calendar.add(Calendar.HOUR, 2);
System.out.println("当前时间加2小时后,时间是:" + calendar.getTime());
calendar.add(Calendar.MONTH, -2);
System.out.println("当前日期减2个月后,时间是:" + calendar.getTime());

1.3 JDK8中新日期时间API

1.3.1 本地日期(LocalDate)、本地时间(LocalTime)、本地日期时间(LocalDateTime)

①. 相关概念

  • 它们的实例是不可变的对象,分别表示使用ISO-8601日历系统(也就是公历)的日期、时间、日期和时间。
  • 它们提供了简单的本地日期或时间,并不包含当前的时间信息,也不包含与时区相关的信息。
  • LocalDate代表IOS格式(yyyy-MM-dd)的日期,可以存储生日、纪念日等日期。
  • LocalTime表示一个时间,而不是日期。
  • LocalDateTime是用来表示日期和时间的,这是一个最常用的类之一

②. 实例化

  • now():静态方法,根据当前时间、日期创建对象/指定时区的对象:LocalDate.now()、LocalTime.now()、LocalDateTime.now()
  • of():静态方法,根据指定日期/时间创建对象:LocalTime.of(23,28,56)、LocalDateTime(2022,05,30,21,06,39)

1.3.2 瞬时(Instant)

  • 时间线上的一个瞬时点,只是简单的表示自1970年1月1日0时0分0秒(UTC)开始的秒数,类似于时间戳,精度可以达到纳秒级。
  • 方法:now(),静态方法,返回默认UTC时区的Instant类的对象

1.3.3 java.time.format.DateTimeFormatter类:格式化与解析日期或时间

①. 预定义的标准格式。如:ISO_LOCAL_DATE_TIME;ISO_LOCAL_DATE;ISO_LOCAL_TIME

DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
LocalDateTime dateTime = LocalDateTime.now();
格式化:日期 → 文本
String format = formatter.format(dateTime);
解析:文本 → 日期
TemporalAccessor parse = formatter.parse(format);

②. 本地化相关的格式。如:ofLocalizedDateTime(FormatStyle.LONG)

③. 自定义的格式。如:ofPattern(“yyyy-MM-dd hh:mm:ss”)

  • 方法:ofPattern(String pattern),静态方法,返 回一个指定字符串格式的DateTimeFormatter

1.3.4 与传统日期处理的转换

1.4 Java比较器

1.4.1 自然排序:java.lang.Comparable

①. 相关概念

  • Comparable接口强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序。
  • 实现Comparable的类必须实现compareTo(Object obj)方法,两个对象即通过compareTo(Object obj)方法的返回值来比较大小。如果当前对象this大于形参对象obj,则返回正整数,如果当前对象this小于形参对象obj,则返回负整数,如果当前对象this等于形参对象obj,则返回零。
  • 实现Comparable接口的对象列表(和数组)可以通过Collections.sort或Arrays.sort进行自动排序
  • 实现此接口的对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器。
  • 对于类C的每一个e1和e2来说,当且仅当e1.compareTo(e2)==0与e1.equals(e2) 具有相同的 boolean值时,类C的自然排序才叫做与equals一致。建议最好使自然排序与equals一致。(虽然不是必需的)

②. Comparable的典型实现:(默认都是从小到大排列的)

  • String:按照字符串中字符的Unicode值进行比较
  • Character:按照字符的Unicode值来进行比较
  • 数值类型对应的包装类以及BigInteger、BigDecimal:按照它们对应的数值大小进行比较
  • Boolean:true 对应的包装类实例大于 false 对应的包装类实例
  • Date、Time等:后面的日期时间比前面的日期时间大

③. 代码案例

class Goods implements Comparable {
    private String name;
    private double price;
    //按照价格,比较商品的大小
    @Override
    public int compareTo(Object o) {
        if(o instanceof Goods) {
            Goods other = (Goods) o;
            if (this.price > other.price) {
                return 1;
            } else if (this.price < other.price) {
                return -1;
            }
            return 0;
        }
        throw new RuntimeException("输入的数据类型不一致");
    }
    //构造器、getter、setter、toString()方法略
}
public class ComparableTest{
    public static void main(String[] args) {
        Goods[] all = new Goods[4];
        all[0] = new Goods("《红楼梦》", 100);
        all[1] = new Goods("《西游记》", 80);
        all[2] = new Goods("《三国演义》", 140);
        all[3] = new Goods("《水浒传》", 120);
        Arrays.sort(all);
        System.out.println(Arrays.toString(all));
    }
}

1.4.2 定制排序:java.util.Comparator

①. 相关概念

  • 当元素的类型没有实现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提供排序。

②. 代码案例、

Goods[] all = new Goods[4];
all[0] = new Goods("War and Peace", 100);
all[1] = new Goods("Childhood", 80);
all[2] = new Goods("Scarlet and Black", 140);
all[3] = new Goods("Notre Dame de Paris", 120);
Arrays.sort(all, new Comparator(){
    @Override
    public int compare(Object o1, Object o2) {
        Goods g1 = (Goods) o1;
        Goods g2 = (Goods) o2;
        // 可以使用compareTo的原因是:String实现了自然排序Comparable
        return g1.getName().compareTo(g2.getName());
    }
});
System.out.println(Arrays.toString(all));

1.5 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的属性对应的值。系统中常见的属性名以及属性的作用如下表所示

③. 代码演示

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);

1.6 Math类

1.7 BigInteger与BigDecimal

1.7.1 BigInteger

①. 相关概念

  • Integer类作为int的包装类,能存储的最大整型值为2^31-1,Long类也是有限的,最大为2^63 -1。如果要表示再大的整数,不管是基本数据类型还是他们的包装类都无能为力,更不用说进行运算了。
  • java.math包的BigInteger可以表示不可变的任意精度的整数。BigInteger提供所有Java的基本整数操作符的对应物,并提供 java.lang.Math的所有相关方法。另外,BigInteger 还提供以下运算:模算术、GCD 计算、质数测试、素数生成、位操作以及一些其他操作。
  • 构造器:BigInteger(String val):根据字符串构建BigInteger对象

②. 常用方法

1.7.2 BigDecimal

一般的Float类和Double类可以用来做科学计算或工程计算,但在商业计算中,要求数字精度比较高,故用到java.math.BigDecimal类。BigDecimal类支持不可变的、任意精度的有符号十进制定点数。

构造器
    public BigDecimal(double val)
    public BigDecimal(String val)
常用方法
    public BigDecimal add(BigDecimal augend)
    public BigDecimal subtract(BigDecimal subtrahend)
    public BigDecimal multiply(BigDecimal multiplicand)
    public BigDecimal divide(BigDecimal divisor, int scale, int roundingMode)

2. Java集合

2.1 Java集合框架概述

①. 数组的特点和弊端

  • 数组在内存存储方面的特点:
    • 数组初始化以后,长度就确定了。
    • 数组声明的类型,就决定了进行元素初始化时的类型
  • 数组在存储数据方面的弊端:
    • 数组初始化以后,长度就不可变了,不便于扩展
    • 数组中提供的属性和方法少,不便于进行添加、删除、插入等操作,且效率不高。同时无法直接获取存储元素的个数
    • 数组存储的数据是有序的、可以重复的。---->存储数据的特点单一

②. 集合的相关概念

  • 为了方便对多个对象的操作,就要对对象进行存储。另一方面,使用Array存储对象方面具有一些弊端,而Java集合就像一种容器,可以动态地把多个对象的引用放入容器中。
  • Java集合类可以用于存储数量不等的多个对象,还可用于保存具有映射关系的关联数组。
  • Java集合可以分为Collection和Map两种体系
  • 注意:凡是存储到集合中的类都要重写equals()方法和hashCode()方法,且要保持一致性:即相等的对象必须具有相等的散列码(哈希值)

③. 集合的分类

  • Collection接口:单列数据,定义了存取一组对象的方法的集合
    • List:元素有序、可重复的集合,实现类:Vector、ArrayList、LinkedList
    • Set:元素无序、不可重复的集合,实现类:HashSet、LinkedHashSet、TreeSet
  • Map接口:双列数据,保存具有映射关系“key-value对”的集合,实现类:Hashtable、Properties、HashMap、LinkedHashMap、TreeMap

2.2 Collection接口方法

  • Collection接口是List、Set和Queue接口的父接口,该接口里定义的方法既可用于操作Set集合,也可用于操作List和Queue集合。
  • JDK不提供此接口的任何直接实现,而是提供更具体的子接口(如:Set和List)实现。
  • 在Java5之前,Java集合会丢失容器中所有对象的数据类型,把所有对象都当成Object类型处理;从JDK5.0增加了泛型以后,Java 集合可以记住容器中对象的数据类型
1、添加
add(Object obj)
addAll(Collection coll)
2、获取有效元素的个数
int size()
3、清空集合
void clear()
4、是否是空集合
boolean isEmpty()
5、是否包含某个元素
boolean contains(Object obj):是通过元素的equals方法来判断是否是同一个对象
boolean containsAll(Collection c):也是调用元素的equals方法来比较的。拿两个集合的元素挨个比较。
6、删除
boolean remove(Object obj) :通过元素的equals方法判断是否是要删除的那个元素。只会删除找到的第一个元素
boolean removeAll(Collection coll):取当前集合的差集
7、取两个集合的交集
boolean retainAll(Collection c):把交集的结果存在当前集合中,不影响c
8、集合是否相等
boolean equals(Object obj)
9、转成对象数组
Object[] toArray()
10、获取集合对象的哈希值
hashCode()
11、遍历
iterator():返回迭代器对象,用于集合遍历

2.3 Iterator迭代器接口

2.3.1 使用Iterator接口遍历集合元素

  • Iterator对象称为迭代器(设计模式的一种),主要用于遍历Collection集合中的元素。
  • 迭代器模式的定义为:提供一种方法访问一个容器(container)对象中各个元素,而又不需暴露该对象的内部细节迭代器模式,就是为容器而生。
  • Collection接口继承了java.lang.Iterable接口,该接口有一个iterator()方法,那么所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对象。
  • Iterator仅用于遍历集合,Iterator本身并不提供承装对象的能力。如果需要创建Iterator对象,则必须有一个被迭代的集合
  • 集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前
迭代器内部的方法:hasNext()和next()搭配使用。
Iterator iterator = coll.iterator();
//hasNext():判断是否还有下一个元素
while(iterator.hasNext()){
    //next():① 指针下移 ② 将下移以后集合位置上的元素返回
    System.out.println(iterator.next());
}
内部还定义了一个remove()方法,Iterator可以删除集合的元素,但是是遍历过程中通过迭代器对象的remove方法,不是集合对象的remove方法

2.3.2 使用foreach循环遍历集合元素

遍历操作不需获取Collection或数组的长度,无需使用索引访问元素。遍历集合的底层调用Iterator完成操作

Collection coll = new ArrayList();
// for(要遍历的元素类型 遍历后自定义的元素名称 : 要遍历的结构名称)
for(Object obj : coll){
    System.out.println(obj);
}

2.4 Collection子接口一:List

2.4.1 List相关概念

  • List集合类中元素有序、可重复。集合中每个元素都有其对应的顺序索引
  • 常使用List替代数组,又称动态数组。
  • List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中元素。
  • JDK API中List接口的实现类常有:ArrayList、LinkedList、Vector
    • ArrayList:作为List接口的主要实现类(jdk1.2),执行效率高(原因:线程不安全的),底层使用Object[] elementData存储
    • LinkedList:对于频繁的插入、删除操作,使用此类效率比ArrayList高,原因是底层使用的双向链表存储(jdk1.2)
    • Vector:作为List接口的古老实现类(jdk1.0),执行效率低(原因:线程安全的),底层使用Object[] elementData存储

2.4.2 List接口方法

List除了从Collection集合继承的方法外,List集合里添加了一些根据索引来操作集合元素的方法

void add(int index,Object ele): 在index位置插入ele元素
boolean addAll(int index,Collection eles): 从index位置开始将eles中的所有元素添加进来
Object get(int index): 获取指定index位置的元素
int indexOf(Object obj): 返回obj在集合中首次出现的位置
int lastIndexOf(Object obj): 返回obj在当前集合中末次出现的位置
Object remove(int index): 移除指定index位置的元素,并返回此元素
Object set(int index,Object ele): 设置指定index位置的元素为ele
List subList(int fromIndex, int toIndex): 返回从fromIndex到toIndex
位置的子集合

2.4.3 ArrayList底层源码分析:JDK7和JDK8中稍有不同

①. JDK7的情况下:长度10,扩容至1.5倍

// 底层创建了长度是10的Object[]数组elementData
ArrayList list = new ArrayList();
// elementData[0] = new Integer(123);
list.add(123);
...
// 如果此次的添加导致底层elementData数组容量不够,则扩容。
默认情况下,扩容为原来容量的1.5倍,同时需要将原有的数据复制到新的数组中
list.add(11);
结论:建议开发中使用带参的构造器,提前指定集合数组容量:
ArrayList list = new ArrayList(int capacity);

②. JDK8的情况下:

// 底层Object[] elementData初始化为{},并没有创建长度为10的数组
ArrayList list = new ArrayList();
// 第一次调用add()时,底层才创建了长度为10的数组,并将数据123添加到elementData[0]
list.add(123);
...
// 如果此次的添加导致底层elementData数组容量不够,则扩容。
默认情况下,扩容为原来容量的1.5倍,同时需要将原有的数据复制到新的数组中
list.add(11);
结论:建议开发中使用带参的构造器,提前指定集合数组容量:
ArrayList list = new ArrayList(int capacity);

③. 小结:JDK7中的ArrayList的对象的创建类似于单例的饿汉式,而JDK8中的ArrayList的创建类似于单例的懒汉式,延迟了数组的创建,相对于节省内存

2.4.4 LinkedList底层源码分析

// 内部没有声明数组,而是定义了Node类型的first和last属性,用于记录首末元素
LinkedList list = new LinkedList();
list.add(123);//将123封装到Node中,创建了Node对象。
    其中,Node定义为:(体现了LinkedList的双向链表)
    private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;
        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }

2.4.5 Vector的源码分析

  • JDK7和JDK8中通过Vector()构造器创建对象时。底层都创建了长度为10的数组。
  • 在扩容方面,默认扩容为原来的数组长度的2倍

2.4.6 ArrayList、LinkedList、Vector的异同

①. ArrayList和LinkedList的异同

  • 二者都线程不安全,相对线程安全的Vector,执行效率高。
  • ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构
  • 对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。
  • 对于新增和删除操作add(特指插入)和remove,LinkedList比较占优势,因为ArrayList要移动数据。

②. ArrayList和Vector的区别

  • Vector和ArrayList几乎是完全相同的,唯一的区别在于Vector是同步类(synchronized),属于强同步类。因此开销就比ArrayList要大,访问要慢。
  • 正常情况下,大多数的Java程序员使用ArrayList而不是Vector,因为同步完全可以由程序员自己来控制。
  • Vector每次扩容请求其大小的2倍空间,而ArrayList是1.5倍。Vector还有一个子类Stack

2.5 Collection子接口二:Set

2.5.1 Set相关概念

  • Set集合:无序、不可重复的(无序指的是存储位置是无序的,但是遍历的时候,可以按照添加顺序遍历)
  • Set接口没有提供额外的方法,使用的都是Collection中定义的方法
  • 向Set中添加的数据,其所在类一定要重写hashCode()和equals()
  • Set判断两个对象是否相同不是使用==运算符,而是根据equals()方法
  • JDK API中Set接口的实现类常有:HashSet、LinkedHashSet、TreeSet
    • HashSet:作为Set接口的主要实现类;线程不安全的;可以存储null值
    • LinkedHashSet:作为HashSet的子类;遍历其内部数据时,可以按照添加的顺序遍历
    • TreeSet:底层使用红黑树(二叉树)的结构存储数据,使其具有特点:添加元素时,必须是同一个类的对象,可以按照对象的指定属性进行排序。

2.5.2 Set实现类之一:HashSet

①. HashSet具有的特点

  • HashSet底层:数组和链表的结构,初始容量为16,当使用率超过0.75,就扩容为原来的2倍
  • 按Hash算法存储集合中的元素,因此具有存储、查找、删除等性能
  • 不能保证元素的排列顺序
  • HashSet线程不安全
  • 集合元素可以为Null
  • HashSet判断两个元素相等的标准是:两个对象通过hashCode()方法比较,并且两个对象的equals()方法返回值也相等,即相同的对象必须具有相等的散列码

②. 向HashSet中添加元素的过程

  • 向HashSet中添加元素a,首先调用a的hashCode()方法,计算元素a的哈希值
  • 根据此哈希值通过某种散列函数计算出在HashSet底层数组中的存放位置
  • 判断数组此位置上是否已经有元素:
    • 如果此位置上没有其他元素时,则元素a直接添加到该位置上
    • 如果此位置上已经有了其他元素b(或以链表形式存在的多个元素),则比较元素a与元素b的hash值:
      • 如果hash值不相同,则元素a添加成功
      • 如果hash值相同,进而需要调用元素a所在类的equals()方法
        • equals()返回true,元素a添加失败
        • equals()返回false,则元素添加成功
  • 对于添加成功的情况而言,元素a与已经存在指定索引位置上数据之间以链表的方式存储
    • jdk7:元素a以头插的方式放在数组的索引位置,指向原来的元素
    • jdk8:原来的元素指向元素a,元素a以尾插的方式放在该索引位置数据链表的结尾

2.5.3 Set实现类之二:LinkedHashSet

  • LinkedHashSet是HashSet的子类
  • LinkedHashSet根据元素的hashCode值来决定元素的存储位置,但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的。
  • LinkedHashSet插入性能略低于HashSet,但在迭代访问Set里的全部元素时有很好的性能
  • LinkedHashSet 不允许集合元素重复

2.5.4 Set实现类之三:TreeSet

  • TreeSet采用红黑树的存储结构,有序,查询速度比List快
  • 如果试图把一个对象添加到TreeSet时,则该对象的类必须实现Comparable接口
  • 向TreeSet中添加元素时,只有第一个元素无须比较compareTo()方法,后面添加的所有元素都会调用compareTo()方法进行比较。
  • 因为只有相同类的两个实例才会比较大小,所以向TreeSet中添加的应该是同一个类的对象
  • 对于TreeSet集合而言,它判断两个对象是否相等的唯一标准是:两个对象通过compareTo(Object obj)方法比较返回值。
  • 当需要把一个对象放入TreeSet中,重写该对象对应的equals()方法时,应保证该方法与 compareTo(Object obj)方法有一致的结果:如果两个对象通过equals()方法比较返回true,则通过 compareTo(Object obj)方法比较应返回0。否则,让人难以理解。

2.6 Map接口

2.6.1 Map接口概述

  • Map与Collection并列存在。用于保存具有映射关系的数据:key-value
  • Map中的key和value都可以是任何引用类型的数据,常用String类作为Map的“键”
  • Map中的key用Set来存放,无序不可重复,value使用Collection存储,无序可重复,即同一个 Map对象所对应的类,须重写hashCode()和equals()方法
  • 一个键值对构成一个Entry对象,且key和value之间存在单向一对一关系,即通过指定的key总能找到唯一的、确定的value
  • Map中的Entry是无序的、不可重复的,使用Set存储所有的entry。
  • Map接口的常用实现类:HashMap、TreeMap、LinkedHashMap和Properties。其中,HashMap是Map接口使用频率最高的实现类
    • HashMap:作为Map的主要实现类;线程不安全的,效率高;可以存储null的key和value
    • LinkedHashMap:保证在遍历map元素时,可以按照添加的顺序实现遍历,对于频繁的遍历操作,此类执行效率高于HashMap。
      • 原因:在原有的HashMap底层结构基础上,添加了双向链表(一对指针),指向前一个和后一个元素
    • TreeMap:保证按照添加的key-value对进行排序,实现排序(按key排)遍历,此时考虑key的自然排序或定制排序。底层使用红黑树
    • Hashtable:作为Map的古老实现类;线程安全的,效率低;不可以存储null的key和value
    • Properties:常用来处理配置文件,key和value都是String类型

2.6.2 Map接口的常用方法

#########添加、删除、修改操作:
Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中
void putAll(Map m):将m中的所有key-value对存放到当前map中
Object remove(Object key):移除指定key的key-value对,并返回value
void clear():清空当前map中的所有数据
#########元素查询的操作:
Object get(Object key):获取指定key对应的value
boolean containsKey(Object key):是否包含指定的key
boolean containsValue(Object value):是否包含指定的value
int size():返回map中key-value对的个数
boolean isEmpty():判断当前map是否为空
boolean equals(Object obj):判断当前map和参数对象obj是否相等
#########元视图操作的方法:
Set keySet():返回所有key构成的Set集合
Collection values():返回所有value构成的Collection集合
Set entrySet():返回所有key-value对构成的Set集合

2.6.3 Map的实现类:HashMap

①. HashMap具有的特点

  • HashMap的存储结构是:HashMap的默认容量是16,扩容是2倍,底层数组是Entry数组
    • JDK7及以前版本:HashMap是数组+链表结构(即为链地址法)
    • JDK8版本发布以后:HashMap是数组+链表+红黑树实现
  • HashMap判断两个key相等的标准是:两个key通过equals()方法返回true,hashCode值也相等。
  • HashMap判断两个value相等的标准是:两个value通过equals()方法返回true

②. HashMap的底层原理(JDK1.8之前)

  • HashMap的内部存储结构其实是数组和链表的结合。
  • 当实例化一个HashMap时,系统会创建一个长度为Capacity的Entry数组,这个长度在哈希表中被称为容量(Capacity)
  • 在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。
  • 每个bucket中存储一个元素,即一个Entry对象,但每一个Entry对象可以带一个引用变量,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Entry链。而且新添加的元素作为链表的head。

③. 添加元素的过程(JDK1.8之前)

#######JDK8之前:
HashMap map = new HashMap();
在实例化以后,底层创建了长度是16的一维数组Entry[] table
...
map.put(key1,value1);
添加元素的过程:
①.首先,调用key1所在类的hashCode()计算key1的哈希值,此哈希值经过某种算法计算
以后,得到在Entry数组中的存放位置
②.判断此位置上是否有元素存在:
    ③.如果此位置上的数据为空,此key1-value1对添加成功
    ④.如果此位置上的数据不为空,(意味着此位置上有一个或多个数据(链表的形式)),则比较key1和已经存在的数据的hash值:
        ⑤.如果比较的hash值不一样,则此时的key1-value1对添加成功
        ⑥.如果比较的hash值一样,则调用key1所在类的equals()方法继续比较:
            如果equals()返回false,则此时的key1-value1对添加成功
            如果equals()返回true,使用value1替换相同key2的value2的值,也就是修改
#######JDK8相教于JDK7在底层实现方面的不同:
①.new HashMap():底层没有创建一个长度为16的数组
②.jdk8底层的数组是:Node[],而非Entry[](本身属性都没有变)
③.首次调用put()方法时,底层创建长度为16的数组Node[]
④.jdk7底层的结构只有:数组 + 链表。jdk8中底层结构:数组 + 链表 + 红黑树。
当数组的某一个索引位置上的元素以链表形式存在的数据个数大于8且当前数组长度超过64时,此时此索引位置上的所有数据改为使用红黑树存储。(遍历快,方便查找(折半查找))

④. HashMap源码中的重要常量

DEFAULT_INITIAL_CAPACITY: HashMap的默认容量,16
MAXIMUM_CAPACITY:HashMap的最大支持容量,2^30
DEFAULT_LOAD_FACTOR:HashMap的默认加载因子,0.75
TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树: 8
UNTREEIFY_THRESHOLD:Bucket中红黑树存储的Node小于该默认值(6),转化为链表
MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量(64)。(当桶中Node
的数量大到需要变红黑树时,若hash表容量小于MIN_TREEIFY_CAPACITY时,此时应执行
resize扩容操作这个MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4
倍。)
table:存储元素的数组,总是2的n次幂
entrySet:存储具体元素的集
size:HashMap中存储的键值对的数量
modCount:HashMap扩容和结构改变的次数。
threshold:扩容的临界值,=容量*填充因子=12
loadFactor:填充因子

2.6.4 Map的实现类:LinkedHashMap

LinkedHashMap是HashMap 的子类
在HashMap存储结构的基础上,使用了一对双向链表来记录添加元素的顺序
与LinkedHashSet类似,LinkedHashMap可以维护Map的迭代顺序:迭代顺序与Key-Value对的插入顺序一致
HashMap中的内部类:Node
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
}
LinkedHashMap中的内部类:Entry
static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

2.7 Collections工具类

Collections中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,
还提供了对集合对象设置不可变、对集合对象实现同步控制等方法
########排序操作:(均为static方法)
reverse(List):反转List中元素的顺序
shuffle(List):对List集合元素进行随机排序
sort(List):根据元素的自然顺序对指定List集合元素按升序排序
sort(List,Comparator):根据指定的Comparator产生的顺序对List集合元素进行排序
swap(List,int i,int j):将指定list集合中的i处元素和j处元素进行交换
########同步控制:Collections类中提供了多个synchronizedXxx()方法该方法可使
将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题
synchronizedCollection(Collection<T> c)
synchronizedList(List<T> list)
synchronizedSet(Set<T> set)
synchronizedMap(Map<K,V> map)

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(已配货)
    • 线程状态:创建、就绪、运行、阻塞、死亡
  • 枚举类的实现
    • JDK1.5之前需要自定义枚举类
    • JDK1.5新增的enum关键字用于定义枚举类
  • 枚举类的属性
    • 枚举类对象的属性不应允许被改动,所以应该使用private final修饰
    • 枚举类的使用private final修饰的属性应该在构造器中为其赋值
    • 若枚举类显示定义了带参数的构造器,则在列出枚举值时也必须对应的传入参数

3.1.2 如何自定义枚举类

  • 私有化类的构造器,保证不能在类的外部创建其对象
  • 在类的内部创建枚举类的实例,声明为public static final
  • 对象如果有实例变量,应该声明为private final,并在构造器中初始化
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("冬天", "白雪皑皑");
}

3.1.3 如何使用关键字enum定义枚举类

  • 使用 enum 定义的枚举类默认继承了 java.lang.Enum类,因此不能再继承其他类
  • 枚举类的构造器只能使用 private 权限修饰符
  • 枚举类的所有实例必须在枚举类中显式列出(, 分隔 ; 结尾)。列出的实例系统会自动添加 public static final 修饰
  • 必须在枚举类的第一行声明枚举类对象
  • JDK1.5中可以在switch表达式中使用Enum定义的枚举类的对象作为表达式,case子句可以直接使用枚举值的名字, 无需添加枚举类作为限定。
public enum SeasonEnum {
    SPRING("春天","春风又绿江南岸"),
    SUMMER("夏天","映日荷花别样红"),
    AUTUMN("秋天","秋水共长天一色"),
    WINTER("冬天","窗含西岭千秋雪");
    private final String seasonName;
    private final String seasonDesc;
    private SeasonEnum(String seasonName, String seasonDesc) {
        this.seasonName = seasonName;
        this.seasonDesc = seasonDesc;
    }
    public String getSeasonName() {
        return seasonName;
    }
    public String getSeasonDesc() {
        return seasonDesc;
    }
}

3.1.4 Enum类的主要方法

  • values()方法:返回枚举类型的对象数组。该方法可以很方便地遍历所有的枚举值。
  • valueOf(String str):可以把一个字符串转为对应的枚举类对象。要求字符串必须是枚举类对象的“名字”。如不是,会有运行时异常:IllegalArgumentException。
  • toString():返回当前枚举类对象常量的名称

3.1.5 实现接口的枚举类

  • 和普通java类一样,枚举类可以实现一个或多个接口
  • 若每个枚举值在调用实现的接口方法呈现相同的行为方式,则只要统一实现该方法即可
  • 若需要每个枚举值在调用实现的接口方法呈现出不同的行为方式,则可以让每个枚举值分别来实现该方法

3.2 注解(Annotation)的使用

3.2.1 注解概述

  • 从JDK5.0开始,java增加了对元数据(MetaData)的支持,也就是Annotation(注解)
  • Annotation其实就是代码里的特殊标记这些标记可以在编译,类加载,运行时被读取,并执行相应的处理。通过使用Annotation,程序员可以在不改变原有逻辑的情况下, 在源文件中嵌入一些补充信息。代码分析工具、开发工具和部署工具可以通过这些补充信息进行验证或者进行部署。
  • Annotation可以像修饰符一样被使用,可用于修饰包,类,构造器,方法,成员变量,参数,局部变量的声明,这些信息被保存在Annotation的 “name=value” 对中
  • 注解较常用,一定程度上来说:框架 = 注解 + 反射 + 设计模式
  • 使用Annotation时要在其前面增加@符号,并把该Annotation当成一个修饰符使用。用于修饰它支持的程序元素

3.2.2 常见的注解示例

①. 生成文档相关的注解

@author 标明开发该类模块的作者,多个作者之间使用,分割
@version 标明该类模块的版本
@see 参考转向,也就是相关主题
@since 从哪个版本开始增加的
@param 对方法中某参数的说明,如果没有参数就不能写
@return 对方法返回值的说明,如果方法的返回值类型是void就不能写
@exception 对方法可能抛出的异常进行说明 ,如果方法没有用throws显式抛出的异常就不能写
其中
@param @return 和 @exception 这三个标记都是只用于方法的。
@param的格式要求:@param 形参名 形参类型 形参说明
@return 的格式要求:@return 返回值类型 返回值说明
@exception的格式要求:@exception 异常类型 异常说明
@param和@exception可以并列多个

②. 在编译时进行格式检查(JDK内置的三个基本注解)

  • @Override:限定重写父类方法, 该注解只能用于方法
  • @Deprecated:用于表示所修饰的元素(类, 方法等)已过时。通常是因为所修饰的结构危险或存在更好的选择
  • @SuppressWarnings:抑制编译器警告

③. 跟踪代码依赖性,实现替代配置文件功能

3.2.3 自定义注解

  • 定义新的 Annotation 类型使用 @interface 关键字
  • 自定义注解自动继承了java.lang.annotation.Annotation 接口
  • Annotation的成员变量在Annotation定义中以无参数方法的形式来声明。其方法名和返回值定义了该成员的名字和类型。我们称为配置参数。类型只能是八种基本数据类型、String 类型 、Class 类型、enum 类型、Annotation 类型、以上所有类型的数组。
  • 可以在定义Annotation的成员变量时为其指定初始值, 指定成员变量的初始值可使用 default 关键字
  • 如果只有一个参数成员,建议使用参数名为value
  • 如果定义的注解含有配置参数,那么使用时必须指定参数值,除非它有默认值。格式是“参数名 = 参数值”,如果只有一个参数成员,且名称为value,可以省略“value=”
  • 没有成员定义的 Annotation称为标记;包含成员变量的Annotation称为元数据Annotation
  • 注意:自定义注解必须配上注解的信息处理流程才有意义
@MyAnnotation(value="尚硅谷")
public class MyAnnotationTest {
    public static void main(String[] args) {
        Class clazz = MyAnnotationTest.class;
        Annotation a = clazz.getAnnotation(MyAnnotation.class);
        MyAnnotation m = (MyAnnotation) a;
        String info = m.value();
        System.out.println(info);
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface MyAnnotation{
    String value() default "auguigu";
}

3.2.4 JDK中的元注解

①. 定义及类型

  • JDK中的元注解用于修饰其他Annotation定义
  • JDK5.0提供了四个标准的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会保留注释。程序可以通过反射获取该注释
  • @Target:用于修饰Annotation定义,用于指定被修饰的Annotation能用于修饰哪些程序元素。@Target也包含一个名为value的成员变量
    • ElementType.CONSTRUCTOR:用于描述构造器
    • ElementType.FIELD:用于描述属性
    • ElementType.LOCAL_VARIABLE:用于描述局部变量
    • ElementType.METHOD:用于描述方法
    • ElementType.PACKAGE:用于描述包
    • ElementType.PARAMETER:用于描述参数
    • ElementType.TYPE:用于描述类、接口(包括注解类型)或enum声明
  • @Documented:用于指定被该元 Annotation 修饰的 Annotation 类将被javadoc工具提取成文档。默认情况下,javadoc是不包括注解的。
    • 定义为Documented的注解必须设置Retention值为RUNTIME。
  • @Inherited被它修饰的Annotation将具有继承性。如果某个类使用了被@Inherited修饰的 Annotation,则其子类将自动具有该注解。
    • 比如:如果把标有@Inherited注解的自定义的注解标注在类级别上,子类则可以继承父类类级别的注解。实际应用中,使用较少

3.2.5 利用反射获取注解信息

3.2.6 JDK8中注解的新特性

4. 多线程

4.1 基本概念

4.1.1 程序、进程、线程

  • 程序是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象
  • 进程是程序的一次执行过程,或正在运行的一个程序。是一个动态的过程,有其自身的产生、存在和消亡的过程——生命周期。进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域
  • 线程:进程可以进一步细化为线程,是一个程序内部的一条执行路径。
    • 若一个进程同一时间并行执行多个线程,就是支持多线程的
    • 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小
    • 一个进程中的多个线程共享相同的内存单元/内存地址空间→它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。

4.1.2 单核CPU和多核CPU的理解

  • 单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程
  • 的任务。但是因为CPU时间单元特别短,因此感觉不出来。
  • 如果是多核的话,才能更好的发挥多线程的效率。
  • 一个Java应用程序java.exe,其实至少有三个线程:main()主线程,gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。

4.1.3 并行与并发

  • 并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
  • 并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事。

4.1.4 多线程的优点

  • 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
  • 提高计算机系统CPU的利用率
  • 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改

4.1.5 何时需要使用多线程

  • 程序需要同时执行两个或多个任务。
  • 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等
  • 需要一些后台运行的程序时

4.1.6 线程的调度策略

  • 时间片
  • 抢占式:高优先级的线程抢占CPU
  • Java的调度方法:
    • 同优先级线程组成先进先出队列(先到先服务),使用时间片策略
    • 对高优先级,使用优先调度的抢占式策略
    • 低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用

4.1.7 线程的优先级

线程的优先级等级
MAX_PRIORITY :10(最大优先级)
MIN _PRIORITY :1(最小优先级)
NORM_PRIORITY :5(正常情况下默认的优先级)
涉及的方法
getPriority():返回线程优先值
setPriority(int newPriority):改变线程的优先级
说明
线程创建时继承父线程的优先级
低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用

4.1.8 java中的线程的分类:守护线程、用户线程

  • 它们在几乎每个方面都是相同的,唯一的区别是判断JVM何时离开
  • 守护线程是用来服务用户线程的,通过在start()方法前调用
  • thread.setDaemon(true)可以把一个用户线程变成一个守护线程。
  • Java垃圾回收就是一个典型的守护线程
  • 若JVM中都是守护线程,当前JVM将退出。

4.2 线程的创建和使用

4.2.1 Thread类

JVM允许程序运行多个线程,通过java.lang.Thread类来体现
每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体
通过该Thread对象的start()方法来启动这个线程,而非直接调用run()
构造器:
Thread():创建新的Thread对象
Thread(String threadname):创建线程并指定线程实例名
Thread(Runnable target):指定创建线程的目标对象,它实现了Runnable接口中的run方法
Thread(Runnable target, String name):创建新的Thread对象
相关方法:
void start(): 启动线程,并执行对象的run()方法
run(): 线程在被调度时执行的操作
String getName(): 返回线程的名称
void setName(String name):设置该线程名称
static Thread currentThread(): 返回当前线程。在Thread子类中就是this,通常用于主线程和Runnable实现类
static void yield():释放当前CPU的执行权,但下一刻可能又被分配到
①.暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
②.若队列中没有同优先级的线程,忽略此方法
join():在线程a中调用线程b的join()方法,此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。
static void sleep(long millis):(指定时间:毫秒)
①.令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。有可能抛出InterruptedException异常
stop(): 强制线程生命期结束,不推荐使用
boolean isAlive():返回boolean,判断线程是否还活着

4.2.2 创建线程的方式一:继承Thread类

①. 创建步骤

  • 定义一个继承Thread类的子类
  • 子类中重写Thread类中的run方法-->将创建的线程要做的事声明在run()方法中
  • 创建Thread子类对象,即创建了线程对象
  • 调用线程对象start方法:启动线程,调用run方法

②. 代码案例:创建三个窗口卖票,总票数为100张

class Window extends Thread{
    private static int ticket = 100;
    @Override
    public void run() {
        while (true){
            if(ticket > 0){
                System.out.println(getName() + ":卖票,票号为:" + ticket);
                ticket--;
            }else{
                break;
            }
        }
    }
}
public class WindowTest {
    public static void main(String[] args) {
        Window w1 = new Window();
        Window w2 = new Window();
        Window w3 = new Window();
        w1.setName("窗口1");
        w2.setName("窗口2");
        w3.setName("窗口3");
        w1.start();
        w2.start();
        w3.start();
    }
}

③. 注意点

  • 如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式。
  • run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU调度决定。
  • 想要启动多线程,必须调用start方法。
  • 一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出IllegalThreadStateException异常

4.2.3 创建线程的方式二:实现Runnable接口

①. 创建步骤

  • 定义子类,实现Runnable接口
  • 子类中重写Runnable接口中的run方法
  • 创建实现类的对象
  • 将此对象作为实际参数传递给Thread类的构造器,创建Thread类的对象
  • 通过Thread类的对象调用start方法:开启线程,调用Runnable子类接口的run方法

②. 代码案例:遍历100以内的所有偶数

//1.创建一个实现了Runnable接口的类
class MThread implements Runnable{
    //2.实现类实现Runnable中的抽象方法:run()
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}
public class ThreadTest1 {
    public static void main(String[] args) {
        //3.创建实现类的对象
        MThread mThread = new MThread();
        // 4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
        Thread t1 = new Thread(mThread);
        //5.通过Thread类的对象调用start()方法:①启动线程 ②调用当前线程的run()
        t1.start();
        //在启动一个线程,遍历100以内的偶数
        Thread t2 = new Thread(mThread);
        t2.start();
    }
}

4.2.4 比较创建线程的两种方式:继承方式和实现方式

  • 开发中,优先选择实现Runnable接口的方式,原因是:
    • 实现的方式没有类的单继承性的局限性
    • 实现的方式更适合处理多个线程有共享数据的情况
  • Thread类本身实现了Runnable接口
  • 两种方式都需要重写run()方法,想要启动线程都需要调用Thread类的start()方法

4.2.5 创建线程的方式三:实现Callable接口(JDK5.0新增)

①. 与使用Runnable相比,Callable功能更强大

  • 重写了call()方法,相比run()方法,可以有返回值
  • 方法可以抛出异常
  • 支持泛型的返回值
  • 需要借助FutureTask类,比如获取返回结果

②. Future接口

  • 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
  • FutureTask是Future接口的唯一实现类
  • FutureTask同时实现了Runnable,Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值

③. 代码案例

//1.创建一个实现Callable的实现类
class NumThread implements Callable<Integer>{
    //2.实现call方法,将此线程需要执行的操作声明在call()中
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 0; i <= 100; i++) {
            if(i % 2 == 0){
                System.out.println(i);
                sum += i;
            }
        }
        return sum;//自动装箱
    }
}
public class ThreadNew {
    public static void main(String[] args) {
        //3.创建Callable接口实现类的对象
        NumThread numThread = new NumThread();
        //4.将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
        FutureTask<Integer> futureTask = new FutureTask<Integer>(numThread);
        //5.将FutureTask类的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
        Thread thread = new Thread(futureTask);
        thread.start();
        try {
            //6.如果对返回值感兴趣,就获取Callable中call方法的返回值。不感兴趣可以不写
            //get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值
            int sum = futureTask.get();
            System.out.println("总和为:" + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

4.2.6 创建线程的方式四:使用线程池(JDK5.0新增)

①. 相关概述

  • 问题:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大
  • 解决思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具
  • 好处:
    • 提高响应速度(减少了创建新线程的时间)
    • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
    • 便于线程管理
      • corePoolSize:核心池的大小
      • maximumPoolSize:最大线程数
      • keepAliveTime:线程没有任务时最多保持多长时间后会终止

②. 线程池相关API:ExecutorService和Executors

ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
①.void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
②.<T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般用来执行Callable
③.void shutdown():关闭连接池
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
①.Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
②.Executors.newFixedThreadPool(n):创建一个可重用固定线程数的线程池
③.Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池
④.Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

③. 代码案例

class NumberThread implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}
class NumberThread1 implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            if(i % 2 == 1){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}
class NumberThread2 implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 0; i <= 100; i++) {
            sum += i;
        }
        return sum;
    }
}
public class ThreadPool {
    public static void main(String[] args) {
        //1.提供指定线程数量的线程池
        //是一个接口,返回一个对象赋给接口service
        ExecutorService service = Executors.newFixedThreadPool(10);
        //获取这个对象属于哪个类
        //System.out.println(service.getClass());//ThreadPoolExecutor
        ThreadPoolExecutor service1 = (ThreadPoolExecutor)service;
        //设置线程池的属性
        //service1.setCorePoolSize(15);
        //service1.setKeepAliveTime();

        //2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(new NumberThread());//适合使用于Runnable
        service.execute(new NumberThread1());//适合使用于Runnable
        //FutureTask<Integer> task = new FutureTask<>(new NumberThread2());
        FutureTask<Integer> task = (FutureTask<Integer>)service.submit(new NumberThread2());//适合使用于Callable
        //方式二:Future<Integer> task = service.submit(new NumberThread2());//适合使用于Callable
        try {
            int sum = task.get();
            System.out.println(Thread.currentThread().getName() + ":" + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        //3.关闭线程池
        service.shutdown();
    }
}

4.3 线程的生命周期(5种状态)

要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:

  • 新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
  • 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
  • 运行当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线程的操作和功能
  • 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态
  • 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束

4.4 线程的同步机制:Synchronized的使用方法

线程的同步解决的是多线程不安全的问题,那么为什么多线程是不安全的呢?因为当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。那么怎么解决呢?对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。

4.4.1 Synchronized的锁

①. 锁的概念

  • 任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)。
  • 同步方法的锁:静态方法(类名.class)、非静态方法(this)
  • 同步代码块:自己指定,很多时候也是指定为this或类名.class
  • 注意:
    • 必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全
    • 一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this),同步代码块(指定需谨慎)

②. 会释放锁的操作

  • 当前线程的同步方法、同步代码块执行结束。
  • 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。
  • 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
  • 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁

③. 不会释放锁的操作

  • 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行
  • 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)。应尽量避免使用suspend()和resume()来控制线程

④. 线程的死锁问题

  • 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
  • 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
public class DeadLockTest {
    public static void main(String[] args) {
        final StringBuffer s1 = new StringBuffer();
        final StringBuffer s2 = new StringBuffer();
        new Thread() {
            public void run() {
                synchronized (s1) {
                    s2.append("A");
                    synchronized (s2) {
                        s2.append("B");
                        System.out.print(s1);
                        System.out.print(s2);
                    }
                }
            }
        }.start();
        new Thread() {
            public void run() {
                synchronized (s2) {
                    s2.append("C");
                    synchronized (s1) {
                        s1.append("D");
                        System.out.print(s2);
                        System.out.print(s1);
                    }
                }
            }
        }.start();
    }
}

4.4.2 方法一:同步代码块

synchronized(对象){
    // 需要被同步的代码;
}
说明:
①.操作共享数据的代码,即为需要被同步的代码-->不能包多了,也不能包少了
②.共享数据:多个线程共同操作的变量
③.同步监视器,俗称:锁。任何一个类的对象,都可以充当锁,但多个线程必须要共用同一把锁
④.在实现Runnable接口创建多线程的方式中,可以考虑使用this充当同步监视器
⑤.在继承Thread类创建多线程的方式中,慎用this充当同步监视器,可以考虑使用当前类充当
代码演示:
class window1 implements Runnable{
    private int ticket = 100;
    private Object obj = new Object();
    @Override
    public void run() {
        while (true){
            synchronized(this){//方式二:synchronized(obj) {
                if (ticket > 0) {
                    System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}
public class WindowTest1 {
    public static void main(String[] args) {
        window1 w = new window1();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

4.4.3 方法二:同步方法

如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明为同步的
private synchronized void xxx(){
    // 需要被同步的代码
}
说明:
①.同步方法仍然涉及到同步监视器,只是不需要显示的声明
②.非静态的同步方法的同步监视器是:this
③.静态的同步方法的同步监视器是:当前类本身
代码演示:
class Window3 implements Runnable{
    private int ticket = 100;
    @Override
    public void run() {
        while(true) {
            show();
            if(ticket <= 0){
                break;
            }
        }
    }
    private synchronized void show(){//默认的同步监视器:this
        if (ticket > 0) {
            System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
            ticket--;
        }
    }
}
public class WindowTest3 {
    public static void main(String[] args) {
        Window3 w = new Window3();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

4.4.4 方法三:lock锁(同步锁),JDK5.0新增

  • Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当
  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
  • ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁
class Window implements Runnable{
    private int ticket = 100;
    //1.实例化ReentrantLock,ReentrantLock是Lock的实现类
    private ReentrantLock lock = new ReentrantLock();//fair:是否公平,不写则是false
    @Override
    public void run() {
        while(true){
            try {
                //2.调用锁定的方法lock(),相当于线程获得同步监视器
                lock.lock();
                if (ticket > 0) {
                    System.out.println(Thread.currentThread().getName() + ",卖票,票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }finally{
                //3.调用解锁的方法unlock()
                lock.unlock();
            }
        }
    }
}
public class LockTest {
    public static void main(String[] args) {
        Window w = new Window();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

4.4.5 synchronized与Lock(ReentrantLock)的异同

  • 相同点:二者都可以解决线程安全问题
  • 不同点:
    • synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器
    • lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())
  • 总结:Lock是显示锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放。Lock只有代码块锁,synchronized有代码块锁和方法锁。使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性。
  • 优先使用顺序:Lock→同步代码块(已经进入了方法体,分配了相应的资源)→同步方法(在方法体之外)

4.5 线程的通信

4.5.1 涉及到的三个方法:wait()、notify()、notifyAll()

  • wait():一旦执行此方法,当前线程就进入阻塞状态(线程挂起并放弃CPU),释放同步监视器。所以调用此方法的必要条件是:当前线程必须具有该对象的监控权(加锁)。在当前线程被notify后,重新获得监控权,然后从断点处继续代码的执行
  • notify():一旦执行此方法,就会唤醒被wait的一个线程,如果有多个线程被wait,就唤醒优先级高的线程
  • notifyAll():一旦执行此方法,就会唤醒所有的被wait的线程

4.5.2 说明

  • wait(),notify(),notifyAll()三个方法只能使用在同步代码块或者同步方法中。Lock不行
  • wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器,否则会出现IllegalMonitorStateException异常
  • wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中

4.5.3 代码:使用两个线程打印1-100。线程1、线程2交替打印

class Number implements Runnable{
    private int number = 1;
    @Override
    public void run() {
        while(true){
            synchronized (this) {
                notify();
                if(number <= 100){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":" + number);
                    number++;
                    try {
                        //使得调用如下wait()方法的线程进入阻塞状态。wait()调用后,会释放锁,其他线程会进来
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }else{
                    break;
                }
            }
        }
    }
}
public class CommunicationTest {
    public static void main(String[] args) {
        Number number = new Number();
        Thread t1 = new Thread(number);
        Thread t2 = new Thread(number);
        t1.setName("线程1");
        t2.setName("线程2");
        t1.start();
        t2.start();
    }
}

5. 泛型

5.1 泛型的概念

  • 所谓泛型,就是允许在定义类、接口时通过一个标识表示类中某个属性的类型或者是某个方法的返回值及参数类型。这个类型参数将在使用时(例如:继承或实现这个接口,用这个类型声明变量、创建对象时)确定(即传入实际的类型参数,也称为类型实参)
  • 从JDK1.5以后,Java引入了“参数化类型(Parameterized type)”的概念,允许我们在创建集合时再指定集合元素的类型,正如:List<String>,这表明该List只能保存字符串类型的对象。
  • JDK1.5改写了集合框架中的全部接口和类,为这些接口、类增加了泛型支持,从而可以在声明集合变量、创建集合对象时传入类型实参。
  • Java泛型可以保证如果程序在编译时没有发出警告,运行时就不会产生ClassCastException异常。同时,代码更加简洁、健壮。

5.2 在集合中使用泛型

ArrayList<Integer> list = new ArrayList<>();//类型推断
list.add(78);
list.add(88);
list.add(77);
list.add(66);
//遍历方式一:
//for(Integer i : list){
//不需要强转
    //System.out.println(i);
//}
//遍历方式二:
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()){
    System.out.println(iterator.next());
}
Map<String,Integer> map = new HashMap<String,Integer>();
map.put("Tom1",34);
map.put("Tom2",44);
map.put("Tom3",33);
map.put("Tom4",32);
//添加失败
//map.put(33, "Tom");
Set<Entry<String,Integer>> entrySet = map.entrySet();
Iterator<Entry<String,Integer>> iterator = entrySet.iterator();
while(iterator.hasNext()){
    Entry<String,Integer> entry = iterator.next();
    System.out.println(entry.getKey() + "--->" + entry.getValue());
}

5.3 自定义泛型结构

①. 泛型的声明

  • interface List<T>和class GenTest<K,V>,其中T,K,V不代表值,而是表示类型。这里使用任意字母都可以。常用T表示,是Type的缩写

②. 泛型的实例化

  • 一定要在类名后面指定类型参数的值(类型)。如:
    • List<String> strList = new ArrayList<String>();
    • Iterator<Customer> iterator = customers.iterator();
  • T只能是类,不能用基本数据类型填充。但可以使用包装类填充
  • 把一个集合中的内容限制为一个特定的数据类型,这就是generics背后的核心思想

③. 泛型类、泛型接口

  • 泛型类可能有多个参数,此时应将多个参数一起放在尖括号内。比如:<E1,E2,E3>
  • 泛型类的构造器如下:public GenericClass(){}。下面是错误的:public GenericClass<E>(){}
  • 实例化后,操作原来泛型位置的结构必须与指定的泛型类型一致。
  • 泛型不同的引用不能相互赋值。尽管在编译时ArrayList<String>和ArrayList<Integer>是两种类型,但是,在运行时只有一个ArrayList被加载到JVM中。
  • 泛型如果不指定,将被擦除,泛型对应的类型均按照Object处理,但不等价于Object。 经验:泛型要使用一路都用。要不用,一路都不要用。
  • 如果泛型结构是一个接口或抽象类,则不可创建泛型类的对象。
  • jdk1.7,泛型的简化操作:ArrayList<Fruit> flist = new ArrayList<>();
  • 泛型的指定中不能使用基本数据类型,可以使用包装类替换
  • 在类/接口上声明的泛型,在本类或本接口中即代表某种类型,可以作为非静态属性的类型、非静态方法的参数类型、非静态方法的返回值类型。但在静态方法中不能使用类的泛型
  • 异常类不能是泛型的
  • 不能使用new E[]。但是可以:E[] elements = (E[])new Object[capacity];参考:ArrayList源码中声明:Object[] elementData,而非泛型参数类型数组。
  • 父类有泛型,子类可以选择保留泛型也可以选择指定泛型类型:
    • 子类不保留父类的泛型:按需实现
      • 没有类型 擦除
      • 具体类型
    • 子类保留父类的泛型:泛型子类
      • 全部保留
      • 部分保留
    • 结论:子类必须是“富二代”,子类除了指定或保留父类的泛型,还可以增加自己的泛型
class Father<T1, T2> {
    // 使用T类型定义变量
    private T1 info;
    // 使用T类型定义一般方法
    public T1 getInfo() {
        return info;
    }
    public void setInfo(T1 info) {
        this.info = info;
    }
    // 使用T类型定义构造器
    public Person() {
    }
    public Person(T1 info) {
        this.info = info;
    }
}
// 子类不保留父类的泛型
// 1)没有类型 擦除
class Son1 extends Father {// 等价于class Son extends Father<Object,Object>{
}
// 2)具体类型
class Son2 extends Father<Integer, String> {
}
// 子类保留父类的泛型
// 1)全部保留
class Son3<T1, T2> extends Father<T1, T2> {
}
// 2)部分保留
class Son4<T2> extends Father<Integer, T2> {
}
##################增加自己的泛型
// 子类不保留父类的泛型
// 1)没有类型 擦除
class Son<A, B> extends Father{//等价于class Son extends Father<Object,Object>{
}
// 2)具体类型
class Son2<A, B> extends Father<Integer, String> {
}
// 子类保留父类的泛型
// 1)全部保留
class Son3<T1, T2, A, B> extends Father<T1, T2> {
}
// 2)部分保留
class Son4<T2, A, B> extends Father<Integer, T2> {
}

④. 泛型方法

  • 方法也可以被泛型化,不管此时定义在其中的类是不是泛型类。在泛型方法中可以定义泛型参数,此时,参数的类型就是传入数据的类型。
  • 泛型方法的格式:【访问权限】<泛型> 返回类型 方法名(【泛型标识 参数名称】) 抛出的异常
public static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
    for (T o : a) {
        c.add(o);
    }
}
public static void main(String[] args) {
    Object[] ao = new Object[100];
    Collection<Object> co = new ArrayList<Object>();
    fromArrayToCollection(ao, co);
    String[] sa = new String[20];
    Collection<String> cs = new ArrayList<>();
    fromArrayToCollection(sa, cs);
    Collection<Double> cd = new ArrayList<>();
    // 下面代码中T是Double类,但sa是String类型,编译错误。
    // fromArrayToCollection(sa, cd);
    // 下面代码中T是Object类型,sa是String类型,可以赋值成功。
    fromArrayToCollection(sa, co);
}

5.4 泛型在继承上的体现

  • 如果B是A的一个子类型(子类或者子接口),而G是具有泛型声明的类或接口,G<B>并不是G<A>的子类型。例如:String是Object类的子类,但是List<String>并不是List<Object>的子类
public void testGenericAndSubClass() {
    Person[] persons = null;
    Man[] mans = null;
    // 而 Person[] 是 Man[] 的父类.
    persons = mans;
    Person p = mans[0];
    // 在泛型的集合上
    List<Person> personList = null;
    List<Man> manList = null;
    // personList = manList;(报错)
}

5.5 通配符的使用

①. 通配符:?

  • 使用类型通配符:?,例如:List<?>,Map<?,?>,List<?>是List<String>、List<Object>等各种泛型List的父类。
  • 读取List<?>的对象List中的元素时,永远是安全的,因为不管List的真实类型是什么,它包含的都是Object
  • 写入List中的元素时,不行。因为我们不知道c的元素类型,我们不能向其中添加对象。唯一的例外是null,它是所有类型的成员
  • 通配符不能用在泛型方法声明上,返回值类型前面<>不能使用?:
    • 错误示范:public static <?> void test(ArrayList<?> list){}
  • 不能用在泛型类的声明上
    • 错误示范:class GenericTypeClass<?>{}
  • 不能用在创建对象上,右边属于创建集合对象
    • 错误示范:ArrayList<?> list1 = new ArrayList<?>();
public static void main(String[] args) {
    List<?> list = null;
    list = new ArrayList<String>();
    list = new ArrayList<Double>();
    // list.add(3);//编译不通过
    list.add(null);
    List<String> l1 = new ArrayList<String>();
    List<Integer> l2 = new ArrayList<Integer>();
    l1.add("尚硅谷");
    l2.add(15);
    read(l1);
    read(l2);
}
public static void read(List<?> list) {
    for (Object o : list) {
        System.out.println(o);
    }
}

②. 有限制的通配符

  • 通配符指定上限:extends,使用时指定的类型必须是继承某个类,或者实现某个接口,即<=
  • 通配符制定下限:super,使用时指定的类型不能小于操作的类,即>=
  • 举例:
    • <? extends Number>:只允许泛型为Number及Number子类的引用调用
    • <?super Number>:只允许泛型为Number及Number父类的引用调用
    • <?extends Comparable>:只允许泛型为实现Comparable接口的实现类的引用调用

5.6 泛型应用举例

①. 泛型嵌套

public static void main(String[] args) {
    HashMap<String, ArrayList<Citizen>> map = new HashMap<String,ArrayList<Citizen>>();
    ArrayList<Citizen> list = new ArrayList<Citizen>();
    list.add(new Citizen("刘恺威"));
    list.add(new Citizen("杨幂"));
    list.add(new Citizen("小糯米"));
    map.put("刘恺威", list);
    Set<Entry<String, ArrayList<Citizen>>> entrySet = map.entrySet();
    Iterator<Entry<String, ArrayList<Citizen>>> iterator = entrySet.iterator();
    while (iterator.hasNext()) {
        Entry<String, ArrayList<Citizen>> entry = iterator.next();
        String key = entry.getKey();
        ArrayList<Citizen> value = entry.getValue();
        System.out.println("户主:" + key);
        System.out.println("家庭成员:" + value);
    }
}

②. 实际案例

6. IO流

6.1 File类的使用

6.1.1 File类的概念

  • 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对象
常用方法:
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数组
public boolean renameTo(File dest):把文件重命名为指定的文件路径
public boolean isDirectory():判断是否是文件目录
public boolean isFile():判断是否是文件
public boolean exists():判断是否存在
public boolean canRead():判断是否可读
public boolean canWrite():判断是否可写
public boolean isHidden():判断是否隐藏
public boolean createNewFile():创建文件。若文件存在,则不创建,返回false
public boolean mkdir():创建文件目录。如果此文件目录存在,就不创建了。如果此文件目录的上层目录不存在,也不创建。
public boolean mkdirs():创建文件目录。如果上层文件目录不存在,一并创建
注意事项:如果你创建文件或者文件目录没有写盘符路径,那么,默认在项目路径下
public boolean delete():删除文件或者文件夹
删除注意事项:Java中的删除不走回收站。要删除一个文件目录,请注意该文件目录内不能包含文件或者文件目录

6.2 IO流原理及流的分类

6.2.1 Java IO原理

  • I/O是Input/Output的缩写,I/O技术是非常实用的技术,用于处理设备之间的数据传输。如读/写文件,网络通讯等
  • java程序中,对于数据的输入输出操作以"流(stream)"的方式进行
  • 输入input:读取外部数据(磁盘、光盘等存储设备的数据)到程序(内存)中
  • 输出output:将程序(内存)数据输出到磁盘、光盘等存储设备中
  • 程序中打开的文件IO资源不属于内存里的资源,垃圾回收机制无法回收该资源,所以应该显式关闭文件IO资源 

6.2.2 流的分类

  • 按操作数据单位不同分为:字节流(8bit)、字符流(16bit)
  • 数据流的流向不同分为:输入流、输出流
  • 流的角色不同分为:节点流、处理流 
    • 节点流:直接从数据源或目的地读写数据
    • 处理流:(提高文件的读写效率)不直接连接到数据源或目的地,而是“连接”在已存在的流(节点流或处理流)之上,通过对数据的处理为程序提供更为强大的读写功能。

 

6.2.3 流的抽象基类

############InputStream(典型实现:FileInputStream)
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:
关闭此输入流并释放与该流关联的所有系统资源
############Reader(典型实现:FileReader)
int read():
读取单个字符。作为整数读取的字符,范围在0到65535之间(0x00-0xffff)(2个
字节的Unicode码),如果已到达流的末尾,则返回 -1
int read(char [] c):
将字符读入数组。如果已到达流的末尾,则返回-1。否则返回本次读取的字符数。
int read(char [] cbuf, int off, int len):
将字符读入数组的某一部分。存到数组cbuf中,从off处开始存储,最多读len个字
符。如果已到达流的末尾,则返回 -1。否则返回本次读取的字符数。
############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:
刷新此输出流并强制写出所有缓冲的输出字节,调用此方法指示应将这些字节立即写入它们预期的目标
############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():
刷新该流的缓冲,则立即将它们写入预期目标。

6.3 节点流(或文件流)

①. 注意点

  • 读取文件时,必须保证该文件已存在,而写出则不需要
  • 对于文本文件(.txt,.java,.c,.cpp),使用字符流处理
  • 对于非文本文件(.jpg,.mp3,.mp4,.avi,.doc,.ppt),使用字节流处理
  • 字节流可以实现文本文件的复制,但不能显示的输出,只能是作为一个中间过渡环节。因为使用字节流FileInputStream处理文本文件,可能出现乱码

②. 代码案例

public class FileInputOutputStreamTest {
    //使用字节流FileInputStream处理文本文件,可能出现乱码
    @Test
    public void test1(){
        FileInputStream fis = null;
        try {
            //1.造文件
            File file = new File("hello.txt");
            //2.造流
            fis = new FileInputStream(file);
            //3.读数据
            byte[] buffer = new byte[5];
            int len;//记录每次读取的字节的个数
            while((len = fis.read(buffer)) != -1){
                String str = new String(buffer,0,len);
                System.out.print(str);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //4.关闭流
            try {
                if(fis != null)
                    fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    /*
    实现对图片的复制操作
    */
    @Test
    public void test2(){
        FileInputStream fis = null;
        FileOutputStream fos = null;
        try {
            //1.创建File类的对象,指明读入和写出的文件
            File srcFile = new File("报名照片.jpg");
            File destFile = new File("报名照片2.jpg");
            //2.创建输入流和输出流的对象
            fis = new FileInputStream(srcFile);
            fos = new FileOutputStream(destFile);
            //3.数据的读入和写出操作:复制
            byte[] buffer = new byte[5];
            int len;//记录每次读入到buffer数组中的字符的个数
            while((len = fis.read(buffer)) != -1){
                //每次写出len个字符
                fos.write(buffer,0,len);
            }
            System.out.println("复制成功");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //4.关闭流资源
            try {
                if(fos != null)
                    fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if(fis != null)
                    fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    //指定路径下文件的复制操作
    public void copyFile(String srcPath,String destPath){
        FileInputStream fis = null;
        FileOutputStream fos = null;
        try {
            //1.创建File类的对象,指明读入和写出的文件
            File srcFile = new File(srcPath);
            File destFile = new File(destPath);
            //2.创建输入流和输出流的对象
            fis = new FileInputStream(srcFile);
            fos = new FileOutputStream(destFile);
            //3.数据的读入和写出操作:复制
            byte[] buffer = new byte[1024];
            int len;//记录每次读入到buffer数组中的字符的个数
            while((len = fis.read(buffer)) != -1){
                //每次写出len个字符
                fos.write(buffer,0,len);
            }
            System.out.println("复制成功");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //4.关闭流资源
            try {
                if(fos != null)
                    fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if(fis != null)
                    fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    @Test
    public void test3(){
        long start = System.currentTimeMillis();

        String srcPath = "C:\\Users\\DELL\\Pictures\\Camera Roll\\1.mp4";
        String destPath = "C:\\Users\\DELL\\Pictures\\Camera Roll\\2.mp4";
        copyFile(srcPath,destPath);

        long end = System.currentTimeMillis();
        System.out.println("复制操作花费的时间为:" + (end - start));//6078
    }
}

6.4 缓冲流

  • 缓冲流的作用:提高流的读取、写入的速度
  • 能够提高读写速度的原因:内部提供了一个缓冲区
  • flush()可以强制将缓冲区的内容全部写入输出流
  • 关闭外层流的同时,内层流也会自动的进行关闭。关于内层流的关闭,我们可以省略
public class BufferedTest {
    //实现文件复制的方法
    public void copyFileWithBuffered(String srcPath,String destPath){
        BufferedInputStream bis = null;
        BufferedOutputStream bos = null;
        try {
            //1.造文件
            File srcFile = new File(srcPath);
            File destFile = new File(destPath);
            //2.造流
            //2.1 造节点流
            FileInputStream fis = new FileInputStream(srcFile);
            FileOutputStream fos = new FileOutputStream(destFile);
            //2.2 造缓冲流
            bis = new BufferedInputStream(fis);
            bos = new BufferedOutputStream(fos);
            //3.复制的细节:读取、写入
            byte[] buffer = new byte[1024];
            int len;
            while((len = bis.read(buffer)) != -1){
                bos.write(buffer,0,len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //4.资源关闭
            //要求:先关闭外层的流,再关闭内层的流
            try {
                if(bos != null)
                    bos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if(bis != null)
                    bis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            //说明:关闭外层流的同时,内层流也会自动的进行关闭。关于内层流的关闭,我们可以省略
            // fos.close();
            // fis.close();
        }
    }
    @Test
    public void testCopyFileWithBuffered(){
        long start = System.currentTimeMillis();
        String srcPath = "C:\\Users\\DELL\\Pictures\\Camera Roll\\1.mp4";
        String destPath = "C:\\Users\\DELL\\Pictures\\Camera Roll\\3.mp4";
        copyFileWithBuffered(srcPath,destPath);
        long end = System.currentTimeMillis();
        System.out.println("复制操作花费的时间为:" + (end - start));//1313
    }
}

6.5 转换流

  • 转换流提供了在字节流和字符流之间的转换
  • Java API提供了两个转换流:
    • InputStreamReader:将InputStream转换为Reader
    • OutputStreamWriter:将Writer转换为OutputStream
  • 字节流中的数据都是字符时,转成字符流操作更高效。
  • 很多时候我们使用转换流来处理文件乱码问题。实现编码和解码的功能

public class InputOutputStreamReaderWriterTest {
    /*
    InputStreamReader的使用,实现字节的输入流到字符的输入流的转换
    */
    @Test
    public void test1(){
        InputStreamReader isr = null;
        try {
            FileInputStream fis = new FileInputStream("dbcp.txt");
            //InputStreamReader isr = new InputStreamReader(fis)//使用系统默认的字符集
            //参数2指明了字符集,具体使用哪个字符集,取决于文件dbcp.txt保存时使用的字符集
            isr = new InputStreamReader(fis,"UTF-8");
            char[] cbuf = new char[20];
            int len;
            while((len = isr.read(cbuf)) != -1){
                String str = new String(cbuf,0,len);
                System.out.print(str);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (isr != null){
                try {
                    isr.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    /*
    综合使用InputStreamReader和OutputStreamWriter
    */
    @Test
    public void test2(){
        InputStreamReader isr = null;
        OutputStreamWriter osw = null;
        try {
            //1.造文件
            File file1 = new File("dbcp.txt");
            File file2 = new File("dbcp_gbk.txt");
            //2.造流
            FileInputStream fis = new FileInputStream(file1);
            FileOutputStream fos = new FileOutputStream(file2);
            isr = new InputStreamReader(fis,"UTF-8");
            osw = new OutputStreamWriter(fos,"gbk");
            //3.读写过程
            char[] cbuf = new char[20];
            int len;
            while ((len = isr.read(cbuf)) != -1){
                osw.write(cbuf,0,len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //4.资源的关闭
            if(osw != null){
                try {
                    osw.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(isr != null){
                try {
                    isr.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

6.6 标准输入、输出流

  • System.in和System.out分别代表了系统标准的输入和输出设备
  • 默认输入设备是:键盘,输出设备是:显示器
  • System.in的类型是InputStream
  • System.out的类型是PrintStream,其是OutputStream的子类、FilterOutputStream 的子类
  • 重定向:通过System类的setIn,setOut方法对默认设备进行改变。
    • public static void setIn(InputStream in)
    • public static void setOut(PrintStream out)

6.7 打印流

  • 实现将基本数据类型的数据格式转化为字符串输出
  • 打印流:PrintStream和PrintWriter
  • 提供了一系列重载的print()和println()方法,用于多种数据类型的输出
  • PrintStream和PrintWriter的输出不会抛IOException异常
  • PrintStream和PrintWriter有自动flush功能
  • PrintStream打印的所有字符都使用平台的默认字符编码转换为字节,在需要写入字符而不是写入字节的情况下,应该使用PrintWriter类
  • System.out返回的是PrintStream的实例

7. 网络编程

7.1 网络编程概述

  • Java提供的网络类库,可以实现无痛的网络连接,联网的底层细节被隐藏在 Java 的本机安装系统里,由 JVM 进行控制。并且 Java 实现了一个跨平台的网络库, 程序员面对的是一个统一的网络编程环境
  • 网络编程的目的:直接或间接的通过网络协议与其他计算机实现数据交换,进行通讯
  • 网络编程的两个主要问题:
    • 如何准确的定位网络上的一台或多台主机或定位主机上的特定的应用
    • 找到主机后如何可靠高效的进行数据传输

7.2 通信要素1:IP和端口号

7.2.1 IP地址:InetAddress

①. IP的相关概念

  • 唯一的标识Internet上的计算机(通信实体)
  • 本地回环地址(hostAddress):127.0.0.1,主机名(hostName):localhost。本地回环地址127.0.0.1指的是本机地址,不会跟着网络情况的变化而变化。它代表设备的本地虚拟接口
  • IP地址分类方式1:IPV4和IPV6
    • IPV4:4个字节组成,4个0-255。大概42亿,30亿都在北美,亚洲4亿。2011年初已经用尽。以点分十进制表示,如192.168.0.1
    • IPV6:128位(16个字节),写成8个无符号整数,每个整数用四个十六进制位表示,数之间用冒号(:)分开,如:3ffe:3201:1401:1280:c8ff:fe4d:db39:1984
  • IP地址分类方式2:公网地址(万维网使用)和私有地址(局域网使用)。192.168.开头的就是私有址址,范围即为192.168.0.0--192.168.255.255,专门为组织机构内部使用

②. InetAddress类

  • Internet上的主机有两种方式表示地址:
    • 域名(hostName):www.atguigu.com
    • IP地址(hostAddress):202.108.35.210
  • InetAddress类主要表示IP地址,两个子类:Inet4Address、Inet6Address。
  • InetAddress类对象含有一个Internet主机地址的域名和IP地址:www.atguigu.com 和 202.108.35.210。
  • 域名容易记忆,当在连接网络时输入一个主机的域名后,域名服务器(DNS)负责将域名转化成IP地址,这样才能和主机建立连接。 ------域名解析

  • InetAddress类没有提供公共的构造器,而是提供了如下几个静态方法来获取InetAddress 实例
    • public static InetAddress getLocalHost()
    • public static InetAddress getByName(String host)
  • InetAddress 提供了如下几个常用的方法
    • public String getHostAddress():返回 IP地址字符串(以文本表现形式)。
    • public String getHostName():获取此IP地址的主机名
    • public boolean isReachable(int timeout):测试是否可以达到该地址
InetAddress address_1 = InetAddress.getByName("www.atguigu.com");
System.out.println(address_1);
// 获取InetAddress对象所含域名
System.out.println(address_1.getHostName());
// 获取InetAddress对象所含IP地址
System.out.println(address_1.getHostAddress());
// 获取本机的域名和IP地址
InetAddress address_2 = InetAddress.getLocalHost();
System.out.println(address_2);

7.2.2 端口号:标识正在计算机上运行的进程(程序)

  • 不同的进程有不同的端口号
  • 被规定为一个16位的整数:0~65535
  • 端口分类:
    • 公认端口:0~1023。被预先定义的服务通信占用(如:HTTP占用端口80,FTP占用端口21,Telnet占用端口23)
    • 注册端口:1024~49151。分配给用户进程或应用程序。(如:Tomcat占用端口8080,MySQL占用端口3306,Oracle占用端口1521等)。
    • 动态/私有端口:49152~65535

7.2.3 端口号与IP地址的组合得出一个网络套接字:Socket

7.3 通信要素2:网络协议

7.3.1 相关概念

  • 网络通信协议:计算机网络中实现通信必须有一些约定,即通信协议,对速率、传输代码、代码结构、传输控制步骤、出错控制等制定标准。有两套参考模型:OSI参考模型、TCP/IP参考模型(或TCP/IP协议)
  • 涉及到的问题:计算机网络通信涉及内容很多,比如指定源地址和目标地址、加密解密、压缩解压缩、差错控制、流量控制、路由控制、如何实现如此复杂的网络协议呢?
  • 通信协议分层的思想:在制定协议时,把复杂成份分解成一些简单的成份,再将它们复合起来。最常用的复合方式是层次方式,即同层间可以通信、上一层可以调用下一层,而与再下一层不发生关系。各层互不影响,利于系统的开发和扩展

7.3.2 TCP/IP协议簇

  • 传输层协议中有两个非常重要的协议:
    • 传输控制协议TCP(Transmission Control Protocol)
    • 用户数据报协议UDP(User Datagram Protocol)
  • TCP/IP以其两个主要协议:传输控制协议(TCP) 和网络互联协议(IP)而得名,实际上是一组协议,包括多个具有不同功能且互为关联的协议。
  • IP(Internet Protocol)协议是网络层的主要协议,支持网间互连的数据通信。
  • TCP/IP协议模型从更实用的角度出发,形成了高效的四层体系结构,即物理链路层、IP层、传输层和应用层

7.3.3 TCP和UDP

①. TCP协议

  • 使用TCP协议前,须先建立TCP连接,形成传输数据通道
  • 传输前,采用“三次握手”方式,点对点通信,是可靠的
  • TCP协议进行通信的两个应用进程:客户端、服务端。
  • 在连接中可进行大数据量的传输
  • 传输完毕,需释放已建立的连接,效率低

②. UDP协议

  • 将数据、源、目的封装成数据包,不需要建立连接
  • 每个数据报的大小限制在64K内
  • 发送不管对方是否准备好,接收方收到也不确认,故是不可靠的
  • 可以广播发送
  • 发送数据结束时无需释放资源,开销小,速度快

③. TCP三次握手

④. TCP四次挥手

7.3.4 网络套接字Socket

①. Socket的相关概念

  • 利用套接字(Socket)开发网络应用程序早已被广泛的采用,以至于成为事实上的标准。
  • 网络上具有唯一标识的IP地址和端口号组合在一起才能构成唯一能识别的标识符套接字。通信的两端都要有Socket,是两台机器间通信的端点。网络通信其实就是Socket间的通信。
  • Socket允许程序把网络连接当成一个流,数据在两个Socket间通过IO传输。
  • 一般主动发起通信的应用程序属客户端,等待通信请求的为服务端。
  • Socket分类:
    • 流套接字(stream socket):使用TCP提供可依赖的字节流服务
    • 数据报套接字(datagram socket):使用UDP提供“尽力而为”的数据报服务

②. Socket类

##############Socket类的常用构造器:
public Socket(InetAddress address,int port)创建一个流套接字并将其连接到指定IP地址的指定端口号。
public Socket(String host,int port)创建一个流套接字并将其连接到指定主机上的指定端口号。
##############Socket类的常用方法:
public InputStream getInputStream()返回此套接字的输入流。可以用于接收网络消息
public OutputStream getOutputStream()返回此套接字的输出流。可以用于发送网络消息
public InetAddress getInetAddress()此套接字连接到的远程IP地址;如果套接字是未连接的,则返回 null。
public InetAddress getLocalAddress()获取套接字绑定的本地地址。即本端的IP地址
public int getPort()此套接字连接到的远程端口号;如果尚未连接套接字,则返回0。
public int getLocalPort()返回此套接字绑定到的本地端口。如果尚未绑定套接字,则返回 -1。即本端的端口号。
public void close()关闭此套接字。套接字被关闭后,便不可在以后的网络连接中使用(即无法重新连接或重新绑定)。需要创建新的套接字对象。关闭此套接字也将会关闭该套接字的InputStream和OutputStream。
public void shutdownInput()如果在套接字上调用shutdownInput()后从套接字输入流读取内容,则流将返回EOF(文件结束符)。即不能在从此套接字的输入流中接收任何数据。
public void shutdownOutput()禁用此套接字的输出流。对于TCP套接字,任何以前写入的数据都将被发送,并且后跟TCP的正常连接终止序列。如果在套接字上调用shutdownOutput()后写入套接字输出流,则该流将抛出IOException。即不能通过此套接字的输出流发送任何数据。

7.4 TCP网络编程

7.4.1 基于Socket的TCP编程

①. 客户端Socket 的工作过程包含以下四个基本的步骤

  • 创建Socket:根据指定服务端的IP地址或端口号构造Socket类对象。若服务器端响应,则建立客户端到服务器的通信线路。若连接失败,会出现异常。
    • 客户端程序可以使用Socket类创建对象,创建的同时会自动向服务器方发起连接。Socket的构造器是:
      • Socket(String host,int port) throws UnknownHostException,IOException:向服务器(域名host、端口号为port)发起TCP连接,若成功,则创建Socket对象,否则抛出异常
      • Socket(InetAddress address,int port) throws IOException:根据InetAddress对象所表示的IP地址以及端口号port发起连接。
    • 客户端建立socketAtClient对象的过程就是向服务器发出套接字连接请求
  • 打开连接到Socket的输入/出流:使用getInputStream()方法获得输入流,使用getOutputStream()方法获得输出流,进行数据传输
  • 按照一定的协议对Socket进行读/写操作:通过输入流读取服务器放入线路的信息(但不能读取自己放入线路的信息),通过输出流将信息写入线程。
  • 关闭Socket:断开客户端到服务器的连接,释放线路

②. 服务端Socket 的工作过程包含以下四个基本的步骤

  • 调用ServerSocket(int port):创建一个服务器端套接字,并绑定到指定端口上。用于监听客户端的请求。服务器必须事先建立一个等待客户请求建立套接字的连接的ServerSocket 对象
  • 调用accept():监听连接请求,如果客户端请求连接,则接受连接,返回通信套接字对象。
  • 调用该Socket类对象的getOutputStream()和getInputStream():获取输出流和输入流,开始网络数据的发送和接收。
  • 关闭ServerSocket和Socket对象:客户端访问结束,关闭通信套接字

7.4.2 代码实现客户端和服务端之间的通信

/**
 * 实现TCP的网络编程
 * 例题3.客户端发送文件给服务端,服务端将文件保存到本地,并返回“发送成功”给客户端,并关闭相应的连接
 *
 * 客户端:
 *      >自定义
 *      >浏览器
 * 服务端:
 *      >自定义
 *      >Tomcat服务器
 * @author wuxy
 * @create 2022-01-16 20:06
 */
public class TCPTest3 {
    //充当客户端
    @Test
    public void client() {
        Socket socket = null;
        OutputStream os = null;
        FileInputStream fis = null;
        InputStream is = null;
        ByteArrayOutputStream baos = null;
        try {
            //1.创建socket对象
            socket = new Socket(InetAddress.getByName("127.0.0.1"), 9090);
            //2.获取输出流
            os = socket.getOutputStream();
            //3.获取输入流
            fis = new FileInputStream(new File("123.jpg"));
            //4.读写过程
            byte[] buffer = new byte[1024];
            int len;
            while ((len = fis.read(buffer)) != -1){
                os.write(buffer,0,len);
            }

            //关闭数据的输出,表示图片已经传完,不再输出数据
            socket.shutdownOutput();

            //客户端接收来自于服务器端的数据,并显示到控制台上
            is = socket.getInputStream();
            baos = new ByteArrayOutputStream();
            byte[] buffer1 = new byte[20];
            int len1;
            while ((len = is.read(buffer1)) != -1){
                baos.write(buffer1,0,len);
            }
            System.out.println(baos.toString());

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //5.资源关闭
            if(baos != null){
                try {
                    baos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(is != null){
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(fis != null){
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(os != null){
                try {
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(socket != null){
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    //充当服务端
    @Test
    public void server() {
        ServerSocket serverSocket = null;
        Socket acceptSocket = null;
        InputStream is = null;
        FileOutputStream fos = null;
        OutputStream os = null;
        try {
            serverSocket = new ServerSocket(9090);
            acceptSocket = serverSocket.accept();
            is = acceptSocket.getInputStream();
            fos = new FileOutputStream(new File("112.jpg"));
            byte[] buffer = new byte[1024];
            int len;
            while((len = is.read(buffer)) != -1){
                fos.write(buffer,0,len);
            }

            System.out.println("图片传输完成");

            //服务器端给予客户端反馈
            os = acceptSocket.getOutputStream();
            os.write("你好,照片我已收到!".getBytes());
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (os != null) {
                try {
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (acceptSocket != null) {
                try {
                    acceptSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (serverSocket != null) {
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

7.5 UDP网络编程

/**
 * 实现UDP网络编程:
 * 1.类DatagramSocket和DatagramPacket实现了基于UDP协议网络程序
 * 2.UDP数据报通过数据报套接字DatagramSocket发送和接收,系统不保证UDP数据报一定能够安全送到目的地,也不能确定什么时候可以抵达。
 * 3.DatagramPacket对象封装了UDP数据报,在数据报中包含了发送端的IP地址和端口号,以及接收端的IP地址和端口号
 * 4.UDP协议中每个数据报都给出了完整的地址信息,因此无需建立发送方和接收方的连接。如同发快递包裹一样
 *
 * @author wuxy
 * @create 2022-01-16 20:45
 */
public class UDPTest1 {
    //发送端
    @Test
    public void send(){
        DatagramSocket socket = null;
        try {
            socket = new DatagramSocket();

            String str = "我是UDP方式发送的数据";
            byte[] data = str.getBytes();
            InetAddress inet = InetAddress.getLocalHost();
            DatagramPacket packet = new DatagramPacket(data,0,data.length,inet,9090);

            socket.send(packet);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(socket != null){
                try {
                    socket.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

    }
    //接收端
    @Test
    public void receiver(){
        DatagramSocket socket = null;
        try {
            socket = new DatagramSocket(9090);
            //创建接收数组,用于存放接收到的数据
            byte[] buffer = new byte[100];
            DatagramPacket packet = new DatagramPacket(buffer,0,buffer.length);

            socket.receive(packet);
            //packet.getData()获取数据,也就是buffer
            System.out.println(new String(packet.getData(),0,packet.getLength()));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(socket != null){
                try {
                    socket.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

7.6 URL编程

7.6.1 URL类

  • URL(Uniform Resource Locator):统一资源定位符,它表示 Internet上某一资源的地址。
  • 它是一种具体的URI,即URL可以用来标识一个资源,而且还指明了如何定位这个资源。
  • 通过URL我们可以访问Internet上的各种网络资源,比如最常见的www,ftp站点。浏览器通过解析给定的URL可以在网络上查找相应的文件或其他资源。
  • URL的基本结构由5部分组成:<传输协议>://< 主机名>:< 端口号>/<文件名>#片段名?参数列表
  • 例如:http://192.168.1.100:8080/helloworld/index.jsp#a?username=shkstart&password=123
  • #片段名:即锚点,例如看小说,直接定位到章节
  • 参数列表格式:参数名=参数值&参数名=参数值

7.6.2 常用方法

构造方法:
public URL(String spec):通过一个表示URL地址的字符串可以构造一个URL对象。
    例如:URL url = new URL("http://www.atguigu.com/");
public URL(URL context, String spec):通过基URL和相对URL构造一个URL对象。
    例如:URL downloadUrl = new URL(url,“download.html")
public URL(String protocol,String host,String file);
    例如:new URL("http","www.atguigu.com",“download. html");
public URL(String protocol,String host,int port,String file); 
    例如: URL gamelan = new URL("http","www.atguigu.com",80,“download.html");
普通方法:
public String getProtocol( ) 获取该URL的协议名
public String getHost( ) 获取该URL的主机名
public String getPort( ) 获取该URL的端口号
public String getPath( ) 获取该URL的文件路径
public String getFile( ) 获取该URL的文件名
public String getQuery( ) 获取该URL的查询名

7.6.3 URI、URL和URN的区别

  • URI,是uniform resource identifier,统一资源标识符,用来唯一的标识一个
  • 资源。
  • URL,是uniform resource locator,统一资源定位符,它是一种具体的URI,即URL可以用来标识一个资源,而且还指明了如何定位这个资源。
  • URN,uniform resource name,统一资源命名,是通过名字来标识资源,比如mailto:java-net@java.sun.com。
  • 也就是说,URI是以一种抽象的,高层次概念定义统一资源标识,而URL和URN则是具体的资源标识的方式。URL和URN都是一种URI
  • 在Java的URI中,一个URI实例可以代表绝对的,也可以是相对的,只要它符合URI的语法规则。而URL类则不仅符合语义,还包含了定位该资源的信息,因此它不能是相对的

8. Java反射机制

8.1 Java反射机制

①. 概述

  • 反射是被视为动态语言的关键,反射机制允许程序在执行期借助于Reflection API取得任何类的内部信息,并能直接操作任意对象的内部属性及方法
  • 加载完类之后,在堆内存的方法区中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象就包含了完整的类的结构信息。我们可以通过这个对象看到类的结构。这个对象就像一面镜子,透过这个镜子看到类的结构,所以,我们形象的称之为: 反射
    • 正常方式:引入需要的“包类”名称 → 通过new实例化 → 取的实例化对象(先有个包,包下声明个类,通过类造对象)
    • 反射方式:实例化对象 → getClass()方法 → 得到完整的“包类”名称(通过对象,找到所在的类,通过所在类的声明,知道哪一个包下造的)

②. Java反射机制提供的功能

  • 在运行时判断任意一个对象所属的类
  • 在运行时构造任意一个类的对象
  • 在运行时判断任意一个类所具有的成员变量和方法
  • 在运行时获取泛型信息
  • 在运行时调用任意一个对象的成员变量和方法
  • 在运行时处理注解
  • 生成动态代理

③. 反射相关的主要API

  • java.lang.Class: 代表一个类
  • java.lang.reflect.Method: 代表类的方法
  • java.lang.reflect.Field: 代表类的成员变量
  • java.lang.reflect.Constructor: 代表类的构造器

8.2 理解Class类并获取Class实例

8.2.1 Class类

  • Class的实例就对应着一个运行时类。加载到内存中的运行时类,会缓存一定的时间,在此时间之内,可以通过不同的方式来获取此运行时类作为Class的实例
  • 对于每个类而言,JRE都为其保留一个不变的Class类型的对象。一个Class对象包含了特定某个结构(class/interface/enum/annotation/primitive type/void/[])的有关信息。
  • Class本身也是一个类
  • Class对象只能由系统建立对象
  • 一个加载的类在JVM中只会有一个Class实例
  • 一个Class对象对应的是一个加载到JVM中的一个.class文件
  • 每个类的实例都会记得自己是由哪个 Class 实例所生成
  • 通过Class可以完整地得到一个类中的所有被加载的结构
  • Class类是Reflection的根源,针对任何想动态加载、运行的类,唯有先获得相应的Class对象

8.2.2 Class类的常用方法

8.2.3 获取Class类的实例(4种方法)

①.前提:若已知具体的类,通过类的class属性获取,该方法最为安全可靠,程序性能最高
实例:Class clazz = String.class;
②.前提:已知某个类的实例,调用该实例的getClass()方法获取Class对象
实例:Class clazz = “www.atguigu.com”.getClass();
③.前提:已知一个类的全类名,且该类在类路径下,可通过Class类的静态方法forName()获取,可能抛出 ClassNotFoundException
实例:Class clazz = Class.forName(“java.lang.String”);
④.其他方式(不做要求)
ClassLoader cl = this.getClass().getClassLoader();
Class clazz4 = cl.loadClass(“类的全类名”);

8.3 类的加载与ClassLoader的理解

8.3.1 类的加载过程

  • 当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过如下三个步骤来对该类进行初始化:
    • 类的加载(Load)→  类的链接(Link)→  类的初始化(Initialize)
  • 加载:将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口(即引用地址)。所有需要访问和使用类数据只能通过这个Class对象。这个加载的过程需要类加载器参与。
  • 链接:将Java类的二进制代码合并到JVM的运行状态之中的过程。
    • 验证:确保加载的类信息符合JVM规范,例如:以cafe开头,没有安全方面的问题
    • 准备:正式为类变量(static)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配。
    • 解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。
  • 初始化:JVM负责对类进行初始化
    • 执行类构造器<clinit>()方法的过程。类构造器<clinit>()方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器)。
    • 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
    • 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步

8.3.2 什么时候会发生类初始化

  • 类的主动引用(一定会发生类的初始化)
    • 当虚拟机启动,先初始化main方法所在的类
    • new一个类的对象
    • 调用类的静态成员(除了final常量)和静态方法
    • 使用java.lang.reflect包的方法对类进行反射调用
    • 当初始化一个类,如果其父类没有被初始化,则先会初始化它的父类
  • 类的被动引用(不会发生类的初始化)
    • 当访问一个静态域时,只有真正声明这个域的类才会被初始化
    • 当通过子类引用父类的静态变量,不会导致子类初始化
    • 通过数组定义类引用,不会触发此类的初始化
    • 引用常量不会触发此类的初始化(常量在链接阶段就存入调用类的常量池中了)

8.3.3 类加载器ClassLoader

  • 类加载的作用:将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口。
  • 类缓存:标准的JavaSE类加载器可以按要求查找类,但一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间。不过JVM垃圾回收机制可以回收这些Class对象
  • 分类
    • 引导类加载器:用C++编写的,是JVM自带的类加载器,负责Java平台核心库,用来装载核心类库。该加载器无法直接获取
    • 扩展类加载器:负责jre/lib/ext目录下的jar包或 –D java.ext.dirs 指定目录下的jar包装入工作库
    • 系统类加载器:负责java –classpath 或 –Djava.class.path所指的目录下的类与jar包装入工作 ,是最常用的加载器
//1.获取一个系统类加载器
ClassLoader classloader = ClassLoader.getSystemClassLoader();
System.out.println(classloader);
//2.获取系统类加载器的父类加载器,即扩展类加载器
classloader = classloader.getParent();
System.out.println(classloader);
//3.获取扩展类加载器的父类加载器,即引导类加载器
classloader = classloader.getParent();
System.out.println(classloader);
//4.测试当前类由哪个类加载器进行加载
classloader = Class.forName("exer2.ClassloaderDemo").getClassLoader();
System.out.println(classloader);
//5.测试JDK提供的Object类由哪个类加载器加载
classloader = Class.forName("java.lang.Object").getClassLoader();
System.out.println(classloader);
//6.关于类加载器的一个主要方法:getResourceAsStream(String str):获取类路
径下的指定文件的输入流
InputStream in = null;
in = this.getClass().getClassLoader().getResourceAsStream("exer2\\test.properties");
System.out.println(in);

8.4 创建运行时类的对象

有了Class对象,可以创建类的对象:调用Class对象的newInstance()方法
要求:
    ①.类必须有一个无参数的构造器
    ②.类的构造器的访问权限需要足够
代码:
方法一:
//获取对应的Class对象
Class<Person> clazz1 = Person.class;
//调用的空参构造器
Person person = clazz1.newInstance();
方法二:
//1.根据全类名获取对应的Class对象
String name = "atguigu.java.Person";
Class clazz = null;
clazz = Class.forName(name);
//2.调用指定参数结构的构造器,生成Constructor的实例
Constructor con = clazz.getConstructor(String.class,Integer.class);
//3.通过Constructor的实例创建对应类的对象,并初始化类属性
Person p2 = (Person)con.newInstance("Peter",20);
System.out.println(p2);

8.5 获取运行时类的完整结构

1.Field:获取属性结构
getFields():获取当前运行时类及其父类中声明为public访问权限的属性
getDeclaredFields():返回此Class对象所表示的类或接口的全部Field(不包含父类中声明的属性)
Field[] fields = clazz.getFields();
public int getModifiers():以整数形式返回此Field的修饰符
public Class<?> getType():得到Field的属性类型
public String getName():返回Field的名称
2.Method:获取所有的方法
getMethods():获取当前运行时类及其所有父类中声明为public权限的方法
getDeclaredMethods():获取当前运行时类中声明的所有方法(不包含父类中声明的方法)
Method[] methods = clazz.getMethods();
public Class<?> getReturnType():取得全部的返回值
public Class<?>[] getParameterTypes():取得全部的参数
public int getModifiers():取得修饰符
public Class<?>[] getExceptionTypes():取得异常信息
3.Constructor:构造器
getConstructors():获取当前运行时类中声明为public的构造器(不包括父类的构造器)
getDeclaredConstructors():获取当前运行时类中声明的所有构造器(不包括父类的构造器)
4.Superclass:所继承的父类
public Class<? Super T> getSuperclass()
返回表示此Class所表示的实体(类、接口、基本类型)的父类的Class。
5.Interface:所实现的接口
public Class<?>[] getInterfaces()
确定此对象所表示的类或接口实现的接口。
6.Annotation:注解
get Annotation(Class<T> annotationClass)
getDeclaredAnnotations()

8.6 调用运行时类的指定结构

1.调用指定方法
① 通过Class类的getMethod(String name,Class…parameterTypes)方法取得一个Method对象,并设置此方法操作时所需要的参数类型。
② 之后使用Object invoke(Object obj, Object[] args)进行调用方法,并向方法中传递要设置的obj对象的参数信息。
2.调用指定属性
public Field getField(String name):返回此Class对象表示的类或接口的指定的
public的Field。
public Field getDeclaredField(String name):返回此Class对象表示的类或接口的
指定的Field。
3.关于setAccessible方法的使用
Method和Field、Constructor对象都有setAccessible()方法。
setAccessible启动和禁用访问安全检查的开关。
参数值为true则指示反射的对象在使用时应该取消Java语言访问检查。
    >提高反射的效率。如果代码中必须用反射,而该句代码需要频繁的被调用,那么请设置为true。
    >使得原本无法访问的私有成员也可以访问
参数值为false则指示反射的对象应该实施Java语言访问检查

8.7 反射的应用:动态代理(略)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值