10 集合

前面我们保存多个数据使用的是数组,数组有不足的地方

  • 长度开始时必须指定,而且一旦指定,不能更改

  • 保存的必须为同一类型的元素

  • 使用数组进行增加/删除元素的示意代码-比较麻烦

而集合有以下优势:

  • 可以动态保存任意多个对象,使用比较方便!

  • 提供了一系列方便的操作对象的方法: add、remove、set、get等

10.1 集合的框架体系

集合框架被设计成要满足以下几个目标。

  • 该框架必须是高性能的。基本集合(动态数组,链表,树,哈希表)的实现也必须是高效的。

  • 该框架允许不同类型的集合,以类似的方式工作,具有高度的互操作性。

  • 对一个集合的扩展和适应必须是简单的。

从上面的集合框架图可以看到,Java 集合框架主要包括两种类型的容器,一种是集合(Collection),存储一个元素集合,另一种是图(Map),存储键/值对映射。Collection 接口又有 3 种子类型,List、Set 和 Queue,再下面是一些抽象类,最后是具体实现类,常用的有 ArrayList、LinkedList、HashSet、LinkedHashSet、HashMap、LinkedHashMap 等等。

// 集合主要是两组(单列集合, 双列集合)
// Collection 接口有两个重要的子接口List Set , 他们的实现子类都是单列集合
// Map 接口的实现子类是双列集合,存放的K-V 类似Python的字典
ArrayList arrayList = new ArrayList();
arrayList.add("jack");
arrayList.add("tom");
​
HashMap hashMap = new HashMap();
hashMap.put("NO1", "北京");
hashMap.put("NO2", "上海");

10.2 Collection 接口和常用方法

10.2.1 Collection 接口实现类的特点

  • collection实现子类可以存放多个元素,每个元素可以是Object

  • 有些Collection的实现类,可以存放重复的元素,有些不可以

  • 有些collection的实现类,有些是有序的(List),有些不是有序(Set)

  • Collection接口没有直接的实现子类,是通过它的子接口Set 和 List来实现的

Collection 接口常用方法

List list = new ArrayList();
// add:添加单个元素
list.add("jack");
list.add(10);
list.add(true);
​
// remove:删除指定元素
list.remove(0);//删除第一个元素
list.remove(true);//指定删除某个元素
​
// contains:查找元素是否存在
System.out.println(list.contains("jack"));
​
// size:获取元素个数
System.out.println(list.size());
​
// isEmpty:判断是否为空
System.out.println(list.isEmpty());
​
// clear:清空
list.clear();
​
// addAll:添加多个元素
ArrayList list2 = new ArrayList();
list2.add("红楼梦");
list2.add("三国演义");
list.addAll(list2);
​
// containsAll:查找多个元素是否都存在
System.out.println(list.containsAll(list2));
​
// removeAll:删除多个元素
list.removeAll(list2);

10.2.2 Collection 接口遍历元素方式

10.2.2.1 使用Iterator(迭代器)

1) lterator对象称为迭代器,主要用于遍历Collection 集合中的元素。 2) 所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对象,即可以返回一个迭代器。 4) lterator仅用于遍历集合,Iterator本身并不存放对象。

ArrayList<String> sites = new ArrayList<String>();
sites.add("Google");
sites.add("Taobao");
sites.add("Zhihu");
​
// 获取迭代器
Iterator<String> it = sites.iterator();
​
// 输出集合中的所有元素
while(it.hasNext()) {
    System.out.println(it.next());
}
  • hasNext()如果还有更多的元素,则返回True

  • next()指针下移,并返回所指的元素

  • 在调用iterator.next()方法之前必须要调用iterator.hasNext()进行检测。若不调用,且下一条记录无效,直接调用it.next()会抛出NoSuchElementException异常。

  • 当退出while 循环后, 这时iterator 迭代器,指向最后的元素,如果希望再次遍历,需要重置我们的迭代器

10.2.2.2 for 循环增强

增强for循环,可以代替iterator迭代器,特点:增强for就是简化版的iterator,本质一样。只能用于遍历集合或数组。

for(元素类型 元素名: 集合名或数组名){
    访问元素
}

例如

List list = new ArrayList();
list.add(new Dog("小黑", 3));
list.add(new Dog("大黄", 100));
list.add(new Dog("大壮", 8));
for (Object dog : list) {
    System.out.println("dog=" + dog);
}

10.3 List 接口和常用方法

List 接口是Collection接口的子接口

  • 1)List集合类中元素有序(即添加顺序和取出顺序一致)、且可重复

  • List集合中的每个元素都有其对应的顺序索引,即支持索引。

  • List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素。

List接口常用的类有ArrayList、LinkedList和Vector

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 位置的子集合[fromIndex <= subList < toIndex]

10.4 ArrayList 底层结构和源码分析

10.4.1 ArrayList 的注意事项

  • permits all elements, including null , ArrayList可以加入null,并且多个

  • ArrayList是由数组来实现数据存储的

  • ArrayList基本等同于Vector,除了ArrayList是线程不安全(执行效率高) 在多线程情况下,不建议使用ArrayList

10.4.2 ArrayList 的底层操作机制源码分析

  • ArrayList中维护了一个Object类型的数组elementData,transient Object[] elementData;表示该属性不会被序列号

  • 当创建ArrayList对象时,如果使用的是无参构造器,则初始elementData容量为0,第1次添加,则扩容elementData为10,如需要再次扩容,则扩容elementData为1.5倍。

  • 如果使用的是指定大小的构造器,则初始elementData容量为指定大小,如果需要扩容,则直接扩容elementData为1.5倍。

10.5 Vector 底层结构和源码剖析

10.5.1 Vector 的基本介绍

public class Vector<E> extends AbstractList<E> implements List<E>, RandomAccess,Cloneable, Seralizeable
  • Vector底层也是一个对象数组 protected Object[] elementData

  • Vector是线程同步的,即线程安全,其操作方法带有synchronized

public stnchronized E get(int index){
    if (index >= elementCount)
        throw new ArrayIndexOutOfBoundsException(index);
    return elementData(index);
}
  • 在开发中,需要线程同步安全时,考虑用Vector

10.5.2 Vector底层扩容机制

Vector vector = new Vector(8);
for (int i = 0; i < 10; i++) {
    vector.add(i);
}
​
// ========= 在底层 ==========
// 方法1 调用无参构造器,初始化空间为10
// public Vector() {
//      this(10);
// }
// 确定是否需要扩容条件: 
// minCapacity - elementData.length>0
// 如果需要的数组大小不够用,就扩容, 扩容的算法(扩容空间为两倍)
// newCapacity = oldCapacity + ((capacityIncrement > 0) ?capacityIncrement : oldCapacity);
// 方法2 调用有参构造器,初始化空间即为传入的空间
// 如果容量不够,则直接按两倍扩容

-

底层结构

线程安全(同步)效率

扩容倍数

ArrayList

可变数组

不安全,效率高

1.5

Vector

可变数组

安全,效率不高

2

10.6 LinkedList 底层结构

10.6.1 LinkedList概述

  • LinkedList底层实现了双向链表和双端队列特点

  • 可以添加任意元素(元素可以重复),包括null

  • 线程不安全,没用实现同步

10.6.2 LinkedList 的底层操作机制

  • LinkedList底层维护了一个双向链表

  • LinkedList中维护了两个属性first和last分别指向首节点和尾节点

  • 每个节点(Node对象),里面又维护了prev、next、item三个属性,最终实现双向链表

  • LinkedList的添加和删除效率较高

// Node 对象, 表示双向链表的一个结点
class Node{
    public Object item;
    public Node next;
    public Node pre;
    public Node(Object name){
        this.item = name;
    }
}

10.6.3 LinkedList 的增删改查案例

LinkedList linkedList = new LinkedList();
linkedList.add(1);
linkedList.add(2);
linkedList.add(3);
​
// 删除结点, 这里默认删除的是第一个结点
linkedList.remove();
// linkedList.remove(2);
​
// 修改某个结点对象
linkedList.set(1, 999);
​
// 得到某个结点对象
Object o = linkedList.get(1);

10.7 Set 接口和常用方法

10.7.1 Set 接口基本介绍

  • 无序(添加和取出的顺序不一致), 没有索引

  • 不允许重复元素,所以最多包含一个null

和List 接口一样, Set 接口也是Collection 的子接口,因此,常用方法和Collection 接口一样.

10.7.2 Set接口遍历

Set set = new HashSet();
set.add("john");
set.add("lucy");
set.add("mary");
set.add(null);
​
// 遍历——迭代器
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
    Object obj = iterator.next();
    System.out.println(obj);
}
​
// 遍历——增强for
for (Object o : set) {
    System.out.println(o);
}

10.7.3 HashSet

HashSet set = new HashSet();
// HashSet 可以存放null ,但是只能有一个null,即元素不能重复
//1. 在执行add 方法后,会返回一个boolean 值
//2. 如果添加成功,返回true, 否则返回false
System.out.println(set.add("john"));//T
System.out.println(set.add("lucy"));//T
System.out.println(set.add("john"));//F
System.out.println(set.add("jack"));//T
System.out.println(set.add("Rose"));//T
//3. 可以通过remove 指定删除哪个对象
set.remove("john");
​
set.add(new Dog("tom"));//OK
set.add(new Dog("tom"));//Ok
​
set.add(new String("hsp"));//ok
set.add(new String("hsp"));//加入不了.

HashSet底层采用数组+链表+红黑树

  • HashSet底层是HashMap

  • 添加一个元素时,先得到hash值-会转成->索引值

  • 找到存储数据表table,看这个索引位置是否已经存放的有元素

  • 如果没有,直接加入

  • 如果有,调用equals比较,如果相同,就放弃添加,如果不相同,则添加到最后

  • 在Java8中,如果一条链表的元素个数到达 TREEIFY THRESHOLD(默认是8),并且table的大小>=MIN TREEIFY CAPACITY(默认64),就会进行树化(红黑树)

10.7.4 LinkedHashSet

  • LinkedHashSet是 HashSet的子类

  • LinkedHashSet底层是一个 LinkedHashMap,底层维护了一个数组+双向链表

  • LinkedHashSet根据元素的hashCode值来决定元素的存储位置,同时使用链表维护元素的次序(图),这使得元素看起来是以插入顺序保存的。

  • LinkedHashSet不允许添重复元素

说明

  • 在LinkedHastSet中维护了一个hash表和双向链表(LinkedHashset有head和tail)

  • 每一个节点有before和after属性,这样可以形成双向链表

  • 在添加一个元素时,先求hash值,在求索引确定该元素在table的位置,然后将添加的元素加入到双向链表(如果已经存在,不添加[原则和hashset一样]

tail.next = newElement
newElement.pre = tail
tail = newEelment;
  • 这样的话。我们遍历LinkedHashSet 也能确保插入顺序和遍历顺序一致

10.8 Map 接口和常用方法

10.8.1 Map 接口实现类的特点

  • Map 与Collection 并列存在。用于保存具有映射关系的数据:Key-Value(双列元素)

  • Map 中的key 和value 可以是任何引用类型的数据,会封装到HashMap$Node 对象中

  • Map 中的key 不允许重复,原因和HashSet 一样,前面分析过源码.

  • Map 中的value 可以重复

  • Map 的key 可以为null, value 也可以为null ,注意key 为null,只能有一个,value 为null ,可以多个

  • 常用String 类作为Map 的key

  • key 和value 之间存在单向一对一关系,即通过指定的key 总能找到对应的value

HashMap hashMap = new HashMap();
hashMap.put("no1", "Java");
hashMap.put("no2", "Python");
hashMap.put("no3", "C++");
System.out.println(hashMap);
  • Map存放数据的key-value示意图,一对k-v是放在一个HashMap$Node中的,有因为Node 实现了Entry 接口,有些书上也说一对k-v就是一个Entry

10.8.2 Map 接口常用方法

// remove:根据键删除映射关系
map.remove(null);
// get:根据键获取值
Object val = map.get("no3");
System.out.println("val=" + val);
// size:获取元素个数
System.out.println("k-v=" + map.size());
// isEmpty:判断个数是否为0
System.out.println(map.isEmpty());
// clear:清除k-v
// map.clear();
System.out.println("map=" + map);
// containsKey:查找键是否存在
System.out.println("结果=" + map.containsKey("Java"));//T

10.8.3 Map 接口遍历方法

// 1.先取出所有的Key , 通过Key 取出对应的Value
Set keyset = map.keySet();
// (1) 增强for
for (Object key : keyset) {
    System.out.println(key + "-" + map.get(key));
}
​
// (2) 迭代器
Iterator iterator = keyset.iterator();
while (iterator.hasNext()) {
    Object key = iterator.next();
    System.out.println(key + "-" + map.get(key));
}
​
// 2.把所有的values 取出
Collection values = map.values();
// (1) 增强for
for (Object value : values) {
    System.out.println(value);
}
​
// (2) 迭代器
Iterator iterator2 = values.iterator();
while (iterator2.hasNext()) {
    Object value = iterator2.next();
    System.out.println(value);
}
​
// 3.通过EntrySet 来获取k-v
Set entrySet = map.entrySet();
// (1) 增强for
for (Object entry : entrySet) {
    //将entry 转成Map.Entry
    Map.Entry m = (Map.Entry) entry;
    System.out.println(m.getKey() + "-" + m.getValue());
}
​
// (2) 迭代器
Iterator iterator3 = entrySet.iterator();
while (iterator3.hasNext()) {
    Object entry = iterator3.next();
    //向下转型Map.Entry
    Map.Entry m = (Map.Entry) entry;
    System.out.println(m.getKey() + "-" + m.getValue());
}

10.9 Map 接口实现类

Map接口的常用实现类:HashMap、Hashtable和Properties。

10.9.1 HashMap

  • HashMap是 Map接口使用频率最高的实现类。

  • HashMap 是以key-val对的方式来存储数据(HashMap$Node类型)

  • key 不能重复,但是值可以重复,允许使用null键和null值

  • 如果添加相同的key,则会覆盖原来的key-val ,等同于修改.(key不会替换,val会替换)

  • 与HashSet一样,不保证映射的顺序,因为底层是以hash表的方式来存储的. (jdk8的hashMap底层数组+链表+红黑树)

  • HashMap没有实现同步,因此是线程不安全的,方法没有做同步互斥的操作,没有synchronized

HashMap的扩容机制和HashSet相同

  • HashMap底层维护了Node类型的数组table,默认为null

  • 当创建对象时,将加载因子(loadfactor)初始化为0.75.

  • 当添加key-val时,通过key的哈希值得到在table的索引。然后判断该索引处是否有元素,如果没有元素直接添加。如果该索引处有元素,继续判断该元素的key和准备加入的key相是否等,如果相等,则直接替换val;如果不相等需要判断是树结构还是链表结构,做出相应处理。如果添加时发现容量不够,则需要扩容。

  • 第1次添加,则需要扩容table容量为16,临界值(threshold)为12 (16*0.75)

  • 以后再扩容,则需要扩容table容量为原来的2倍(32),临界值为原来的2倍,即24,依次类推

  • 在Java8中,如果一条链表的元素个数超过TREEIFY_THRESHOLD(默认是8),粗table的大小 >= MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树)

10.9.2 Hashtable

  • 存放的元素是键值对:即K-V

  • hashtable的键和值都不能为null,否则会抛出NullPointerException

  • hashTable使用方法基本上和HashMap一样

  • hashTable是线程安全的(synchronized), hashMap是线程不安全的

10.9.3 Properties

  • Properties类继承自Hashtable类并且实现了Map接口,也是使用一种键值对的形式来保存数据。

  • 他的使用特点和Hashtable类似

  • Properties还可以用于从 xxx.properties文件中,加载数据到Properties类对象,并进行读取和修改

说明:工作后xxx.properties文件通常作为配置文件,这个知识点在IO流举例

10.10 Collections 工具类

Collections是一个操作 Set、List和 Map等集合的工具类

Collections中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作

// reverse(List):反转List 中元素的顺序
Collections.reverse(list);
// shuffle(List):对List 集合元素进行随机排序
Collections.shuffle(list);
// sort(List):根据元素的自然顺序对指定List 集合元素按升序排序
Collections.sort(list);
// sort(List,Comparator):根据指定的Comparator 产生的顺序对List 集合元素进行排序
Collections.sort(list, new Comparator() {
    @Override
    public int compare(Object o1, Object o2) {
        return ((String) o2).length() - ((String) o1).length();
    }
});
// swap(List,int, int):将指定list 集合中的i 处元素和j 处元素进行交换
Collections.swap(list, 0, 1);
// Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
Collections.max(list);
// Object max(Collection,Comparator)根据Comparator 指定的顺序,返回给定集合中的最大元素
Object maxObject = Collections.max(list, new Comparator() {
    @Override
    public int compare(Object o1, Object o2) {
        return ((String)o1).length() - ((String)o2).length();
    }
});
// Object min(Collection)
// Object min(Collection,Comparator)
// int frequency(Collection,Object):返回指定集合中指定元素的出现次数
Collections.frequency(list, "tom");
// void copy(List dest,List src):将src 中的内容复制到dest 中
Collections.copy(dest, list);
// boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换List 对象的所有旧值
Collections.replaceAll(list, "tom", "汤姆");

11 泛型

Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。

泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。使用 Java 泛型的概念,我们可以写一个泛型方法来对一个对象数组排序。然后,调用该泛型方法来对整型数组、浮点数数组、字符串数组等进行排序。

11.1 泛型方法

你可以写一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。

下面是定义泛型方法的规则:

  • 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前。

  • 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。

  • 类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。

  • 泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(像 int、double、char 等)。

java 中泛型标记符:

  • E - Element (在集合中使用,因为集合中存放的是元素)

  • T - Type(Java 类)

  • K - Key(键)

  • V - Value(值)

  • N - Number(数值类型)

  • - 表示不确定的 java 类型

T和E必须是引用类型,如果是基本类型就会报错

在给泛型指定具体类型后,可以传入该类型或者其子类类型

public class generic01 {
    public static void main(String[] args) {
        ArrayList<Dog> arrayList = new ArrayList<Dog>();
        arrayList.add(new Dog(10, "小黄"));
        arrayList.add(new Dog(12, "小黑"));
        arrayList.add(new Dog(15, "小懒"));
        for (Dog o :arrayList) {
            System.out.println(o.getName() + "-" + o.getAge());
        }
​
    }
}
​
class Dog {
    public int age;
    public String name;
​
    public Dog(int age, String name) {
        this.age = age;
        this.name = name;
    }
​
    public int getAge() {
        return age;
    }
​
    public String getName() {
        return name;
    }
​
    @Override
    public String toString() {
        return "Dog{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}

编译时,检查添加元素的类型,提高了安全性

减少了类型转换的次数,提高效率

11.2 泛型类

泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。

和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。

public class generic02 {
    public static void main(String[] args) {
        Person<String> objectPerson = new Person<String>("user");
        System.out.println(objectPerson.toString());
    }
}
​
class Person<E>{
    private final E name;
​
    public Person(E name) {
        this.name = name;
    }
​
    public E func(){
        return name;
    }
​
    @Override
    public String toString() {
        return "Person{" +
                "name=" + name +
                '}';
    }
}

11.3 泛型的通配符

泛型不具备继承性,但可以通过通配符限制泛型类型

  • <?>: 支持任意泛型类型

  • <? extends A>:支持A类以及A类的子类,规定了泛型的上限

  • <? super A>:支持A类以及A类的父类,不限于直接父类,规定了泛型的下限

public class generic03 {
    public static void main(String[] args) {
        ArrayList<Object> list1 = new ArrayList<>();
        ArrayList<String> list2 = new ArrayList<>();
        ArrayList<AA> list3 = new ArrayList<>();
        ArrayList<BB> list4 = new ArrayList<>();
        ArrayList<CC> list5 = new ArrayList<>();
​
        // 如果是List<?> c ,可以接受任意的泛型类型
        printCollection1(list1);
        printCollection1(list2);
        printCollection1(list3);
        printCollection1(list4);
        printCollection1(list5);
​
        // List<? extends AA> c: 表示上限,可以接受AA 或者AA 子类
        // printCollection2(list1);
        // printCollection2(list2);
        printCollection2(list3);
        printCollection2(list4);
        printCollection2(list5);
​
        // List<? super AA> c: 支持AA 类以及AA 类的父类,不限于直接父类
        printCollection3(list1);
        // printCollection3(list2);
        printCollection3(list3);
        // printCollection3(list4);
        // printCollection3(list5);
    }
​
    public static void printCollection1(List<?> c){
        for (Object o :c) {
            System.out.println(o);
        }
​
    }
​
    public static void printCollection2(List<? extends AA> c){
        for (Object o :c) {
            System.out.println(o);
        }
​
    }
​
    public static void printCollection3(List<? super AA> c){
        for (Object o :c) {
            System.out.println(o);
        }
​
    }
}
​
class AA{}
class BB extends AA{}
class CC extends BB{}

12 多线程基础

线程相关概念见操作系统部分,这里不再阐述

12.1 创建线程的方式-继承Thread类

创建一个线程的第一种方法是创建一个新的类,该类继承 Thread 类,然后创建一个该类的实例。

继承类必须重写 run() 方法,该方法是新线程的入口点。它也必须调用 start() 方法才能执行。

该方法尽管被列为一种多线程实现方式,但是本质上也是实现了 Runnable 接口的一个实例。

public class thread_ {
​
    public static void main(String[] args) {
        Runtime runtime = Runtime.getRuntime();
        int cpuNums = runtime.availableProcessors();
        System.out.println(cpuNums); // 8核CPU
​
        Cat cat = new Cat("oamfoawmw");
        cat.start();
​
    }
}
​
class Cat extends Thread{
    private int times = 0;
    String s;
​
    public Cat(String s){
        this.s = s;
    }
​
    @Override
    public void run() {
        System.out.println(s);
        do {
            System.out.println("cat~ cat~" + (++times));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } while (times != 10);
    }
}

start()方法调用start0()方法后,该线程并不一定会立马执行,只是将线程变成了可运行状态。具体什么时候执行,取决于CPU,由CPU统一调度。

12.2 创建线程的方式-实现Runnable接口

  1. java是单继承的,在某些情况下一个类可能已经继承了某个父类,这时在用继承Thread类方法来创建线程显然不可能了。

  2. java设计者们提供了另外一个方式创建线程,就是通过实现Runnable接口来创建线程

public class thread2_ {
    public static void main(String[] args) {
        Dog dog = new Dog();
        Thread thread = new Thread(dog);
        thread.start();
    }
}
​
class Dog implements Runnable{
    int count = 0;
​
    @Override
    public void run() {
        while (true){
            System.out.println("dog~ dog~" + (++count));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
​
            if (count == 10) break;
        }
​
    }
}

12.3 线程终止

  • 当线程完成任务后,会自动退出

  • 还可以通过使用变量来控制run方法退出的方式停止线程,即通知方式

public class SellTicket{
    public static void main(String[] args) throws InterruptedException {
        T t1 = new T();
        t1.start();
        
        Thread.sleep(10 * 1000);
        t1.setLoop(false);
    }
}
​
class T extends Thread {
    private int count = 0;
    private boolean loop = true;
    @Override
    public void run() {
        while (true){
            System.out.println((++count));
​
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    public void setLoop(boolean loop){
        this.loop = loop;
    }
    
}

12.4 线程的常用方法

// setName 设置线程名称
// getName 返回该线程的名称
// start 使该线程开始指向;虚拟机底层调用该线程的start0方法
// run 调用线程对象run方法
// setPriority 更改线程的优先级
// getPriority 获取线程的优先级
// sleep 在指定的毫秒数内让当前正在执行的线程休眠
// interrupt 中断线程,但没有真正的结束线程
// yield 线程的礼让,让出cpu,让其他线程执行,但礼让的时间不确定,所以也不一定礼让成功
// join 线程的插队,插队的进程一旦插队成功,则肯定先执行插入的线程所有的任务

12.5 用户线程和守护线程

  • 用户线程:也叫工作线程,当线程的任务执行完或通知方式结束

  • 守护线程:一般是为工作线程服务的,当所有的用户线程结束,守护线程自动结束

  • 常见的守护线程:垃圾回收机制

public class Methods{
    public static void main(String[] args) {
        Tes tes = new Tes();
        // 设置为守护线程
        tes.setDaemon(true);
        tes.start();
    }
}
​
class Tes extends Thread{
    
}

12.6 线程的生命周期

线程状态。一个线程可以有以下规定:

  • NEW:尚未启动的进程处于此状态

  • RUNNABLE:处于这种状态中的java虚拟机执行的线程。

  • BLOCKED:线程阻塞等待监控锁定。

  • WAITING:处于这种状态的线程被无限期地等待另一个线程来执行特定的动作。

  • TIMED_WAITING:正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。

  • TERMINATED:已退出的线程处于这种状态。

具体见操作系统部分

12.7 线程的同步- Synchronized

  • 在多线程编程,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何同一时刻,最多有一个线程访问,以保证数据的完整性。

  • 线程同步,即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作.

public class SellTicket{
    public static void main(String[] args) throws InterruptedException {
        Sell1 sell1 = new Sell1();
​
        new Thread(sell1).start();
        new Thread(sell1).start();
        new Thread(sell1).start();
    }
}
​
class Sell1 implements Runnable {
​
    private static int ticketNum = 100;
​
    @Override
    public void run() {
        while (true){
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
​
            System.out.println("now has " + (ticketNum) + " tickets");
            if (sell()) break;
        }
    }
​
    public synchronized boolean sell(){
        if (ticketNum <= 0){
            System.out.println("over...");
            return true;
        }
        System.out.println("Windows" + Thread.currentThread().getName() + "sale 1 ticket!");
        ticketNum --;
        return false;
    }
}
  • Java语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性.

  • 每个对象都对应于一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。

  • 关键字synchronized来与对象的互斥锁联系。当某个对象用synchronized修饰时,表明该对象在任一时刻只能由一个线程访问

  • 同步的局限性:导致程序的执行效率要降低

  • 同步方法(非静态的)的锁可以是this,也可以是其他对象(要求是同一个对象)

  • 同步方法(静态的)的锁为当前类本身。

同步方法如果没有使用static修饰:默认锁对象为this

如果方法使用static修饰,默认锁对象:当前类.class

class Sell1 implements Runnable {
​
    private static int ticketNum = 100;
​
    @Override
    public void run() {
        while (true){
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
​
            System.out.println("now has " + (ticketNum) + " tickets");
            
            synchronized(this){
                if (ticketNum <= 0){
                    System.out.println("over...");
                    break;
                }
                System.out.println("Windows" +                  Thread.currentThread().getName() + "sale 1 ticket!");
                ticketNum --;
            }
        }
    }
}

13 IO流

13.1 文件的概念

文件在程序中是以流的形式来操作的

  • 流:数据在数据源(文件)和程序(内存)之间经历的路径

  • 输入流:数据从数据源(文件)到程序(内存)的路径

  • 输出流:数据从程序(内存)到数据源(文件)的路径

13.2 常用的文件操作

13.2.1 创建文件对象相关构造器和方法

// 根据路径构建一个File对象
new File(String pathname);
// 根据父目录文件+子路径构建
new File(File parent, String child);
// 根据父目录+子路径构建
new File(String parent, String child);
​
// 创建新文件
File.createNewFile()

13.2.2 获取文件的相关信息

// 获取文件的名字
File.getName();
// 得到文件的绝对路径
File.getAbsolutePath();
// 获取文件的父级目录
File.getParent();
// 获取文件的大小,以字节为单位
File.length();
// 判断文件是否存在
File.exists();
// 判断是否是一个文件
File.ifFile();
// 判断是否是一个目录
File.isDirectory();

13.2.3 目录的操作和文件删除

// 删除空目录或文件
File.delete();
// 创建一级目录
File.mkdir();
// 创建多级目录
File.mkdirs();

13.3 IO 流原理及流的分类

13.3.1 Java IO 流原理

  • I/O是Input/Output的缩写,I/O技术是非常使用的技术,用于处理数据的传输。如读写文件,网络通讯等

  • Java程序中,对于数据的输入/输出以流(stream)的方式进行

  • Java.io包下提供了各种流类和接口,用以获取不同种类的数据,并通过方法输入或输出数据

  • 输入input:读取外部数据(磁盘、光盘等存储设备的数据)到程序(内存)中

  • 输出output:将程序(内存)数据输出到磁盘、光盘等存储设备中

13.3.2 流的分类

  • 按操作数据单位不同分为:字节流(8bit)二进制文件,字符流(按字符)文本文件

  • 按数据流的流向不同可分为:输入流,输出流

  • 按流的角色不同可分为:节点流,处理流/包装流

抽象基类

字节流

字符流

输入流

InputStream

Reader

输出流

OutputStream

Writer

13.4 字节流

13.4.1 FileInputStream

public static void readFile() throws IOException {
    String filePath = ".\\1.txt";
    int readData = 0;
    FileInputStream fileInputStream = new FileInputStream(filePath);
    try {
        while ((readData = fileInputStream.read()) != -1){
            System.out.print((char) readData);
        }
​
    } catch (IOException e){
        e.printStackTrace();
    } finally {
        fileInputStream.close();
    }
}

或者

public static void readFile() throws IOException {
    String filePath = ".\\1.txt";
    int readLen;
    byte[] buf = new byte[8];
    FileInputStream fileInputStream = new FileInputStream(filePath);
    try {
        while ((readLen = fileInputStream.read(buf)) != -1){
            System.out.print(new String(buf, 0, readLen));
        }
    } catch (IOException e){
        e.printStackTrace();
    } finally {
        fileInputStream.close();
    }
}

13.4.2 FileOutputStream

public static void writeFile() throws IOException {
    String filePath = ".\\2.txt";
    FileOutputStream fileOutputStream = null;
    try {
        // fileOutputStream = new FileOutputStream(filePath, true);
        fileOutputStream = new FileOutputStream(filePath);
        // fileOutputStream.write('a');
        String str = "Hello, world!";
        // fileOutputStream.write(str.getBytes());
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } finally {
        fileOutputStream.close();
    }
}

13.5 字符流

13.5.1 FileReader

new FileReader(File|String);

// 每次读取单个字符,返回该字符,如果到文件末尾返回-1
FileReader.read();
// 批量读取多个字符到数组,返回读取到的字符数,如果到文件末尾返回-1
FileReader.read(char[]);

例子:

public static void fileShow() throws IOException{
    String filePath = ".\\1.txt";
    FileReader fileReader = null;
    char[] buf = new char[8];
    int data;
    try {
        fileReader = new FileReader(filePath);
        while ((data = fileReader.read(buf)) != -1){
            System.out.print(new String(buf, 0, data));
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        fileReader.close();
    }
}

13.5.2 FileWriter

new FileWriter(File|String); 	// 覆盖模式,相当于流的指针在首端
new FileWriter(Fuke|String, true);// 追加模式,相当于流的指针在尾端

// 写入单个字符
FileWriter.writer(int);
// 写入指定数组
FileWriter.writer(char[]);
// 写入指定数组的指定部分
FileWriter.writer(char[], off, len);
// 写入整个字符串
FileWriter.writer(string);
// 写入字符串的指定部分
FileWriter.writer(string, off, len)

例子

public static void fileWriter() throws IOException{
    String filePath = ".\\2.txt";
    FileWriter fileWriter = null;
    char[] chars = {'a', 'b', 'c'};
    try {
        fileWriter = new FileWriter(filePath);
        fileWriter.write(chars);
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        fileWriter.close();
    }
}

13.6 节点流和处理流

  • 节点流可以从一个特点的数据源读取数据,如FileReader、FileWriter

  • 处理流(也叫包装流)是链接在已存在的流(节点流或处理流)之上,为程序提供更为强大的读写功能,也更加灵活。如BufferedReader、BufferedWriter

    • 性能的提高:主要以增加缓冲的方式来提高输入输出的效率。

    • 操作的便捷:处理流可能提供了一系列便捷的方法来一次输入输出大批量的数据,使用更加灵活方便

  • 节点流是底层流/低级流,直接跟数据源相接。

  • 处理流(包装流)包装节点流,既可以消除不同节点流的实现差异,也可以提供更方便的方法来完成输入输出。

  • 处理流(也叫包装流)对节点流进行包装,使用了修饰器设计模式,不会直接与数据源相连

13.7 处理流

13.7.1 BufferedReader和BufferedWriter

  • BufferedReader和BufferedWriter属于字符流,是按照字符来读取数据的

  • 关闭时处理流,只需要关闭外层流即可

13.7.1.1 BufferedReader

public static void main(String[] args) throws Exception {
    String filePath = ".\\Input.java";
    BufferedReader bufferedReader = new BufferedReader(new FileReader(filePath));
    String line;
    // read表示按行读取文件,当返回为null时,说明读取结束
    while ((line = bufferedReader.readLine()) != null) {
        System.out.println(line);
    }
    // 关闭流,只需要关闭bufferedReader即可,底层会自动关闭节点流
    bufferedReader.close();
}

13.7.1.2 BufferedWriter

public static void main(String[] args) throws Exception {
    String filePath = ".\\1.txt";
    BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(filePath));
    // 如果采用追加的话,采用以下构造器
    // BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(filePath, true));
    for (int i = 0; i < 5; i++) {
        bufferedWriter.write(new String("times" + i));
        // 插入一个和系统相关的换行符
        bufferedWriter.newLine();
    }
    bufferedWriter.close();
}

13.7.2 BufferedInputStream和BufferedOutputStream

二进制文件的操作一般采用这两个流

  • BufferedInputStream是字节流在创建BufferedlnputStream时,会创建一个内部缓冲区数组.

  • BufferedOutputStream是字书流,实现缓冲的输出流,可以将多个字节写入底层输出流中,而不必对每次字节写入调用底层系统

public static void main(String[] args) {
    String srcFilePath = ".\\1.jpg";
    String destFilePath = ".\\2.jpg";
​
    BufferedInputStream bis = null;
    BufferedOutputStream bos = null;
​
    try {
        bis = new BufferedInputStream(new FileInputStream(srcFilePath));
        bos = new BufferedOutputStream(new FileOutputStream(destFilePath));
​
        byte[] buff = new byte[1024];
        int readLen;
​
        while ((readLen = bis.read(buff)) != -1) {
            bos.write(buff, 0, readLen);
        }
​
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        // 关闭外层流即可
        try {
            if (bis != null) bis.close();
            if (bos != null) bos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

13.7.3 ObjectInputStream和ObjectOutputStream

序列化和反序列化

  • 序列化就是在保存数据时,保存数据的值和数据类型

  • 反序列化就是在恢复数据时,恢复数据的值和数据类型

  • 需要让某个对象支持序列化机制,则必须让其类是可序列化的,为了让某个类是可序列化的,该类必须实现如下两个接口之一:

    • Serializable 这是一个标记接口,没有方法

    • Externalizable 该接口有方法需要实现,因此我们一般实现上面的Serializable接口

ObjectInputStream和ObjectOutputStream提供了对基本类型或对象类型的序列化和反序列化的方法

  • ObjectOutputStream 提供序列化功能

public class ObjectOutStream {
    public static void main(String[] args) throws Exception {
        String filePath = ".\\data.dat";
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath));
​
        // 序列化数据
        oos.writeInt(100); // int -> Integer
        oos.writeBoolean(true); // boolean -> Boolean
        oos.writeChar('a'); // char -> Character
        oos.writeObject(new Dog("旺财", 10));
​
        oos.close();
    }
}
​
class Dog implements Serializable {
    private String name;
    private int age;
​
    public Dog(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
  • ObjectInputStream 提供反序列化功能

public static void main(String[] args) throws Exception{
    String filePath = ".\\data.dat";
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filePath));

    // 读取反序列化的数据需要和保存的序列化的数据顺序一致,否则会出现异常
    System.out.println(ois.readInt());
    System.out.println(ois.readBoolean());
    System.out.println(ois.readChar());
    System.out.println(ois.readObject());

    ois.close();
}

1)读写顺序要一致

2)要求序列化或反序列化对象,需要实现 Serializable

3)序列化的类中建议添加SerialversionUID,为了提高版本的兼容性

4)序列化对象时,默认将里面所有属性都进行序列化,但除了static或transient修饰的成员

5)序列化对象时,要求里面属性的类型也需要实现序列化接口

6)序列化具备可继承性,也就是如果某类已经实现了序列化,则它的所有子类也已经默认实现了序列化

13.7.4 InputStreamReader和OutputStreamWriter

InputStreamReader和OutputStreamWriter称为转换流,属于处理流

  • InputStreamReader:Reader的子类,可以将InputStream(字节流)包装成(转换)Reader(字符流)

  • OutputStreamWriter:Writer的子类,实现将OutputStream(字节流)包装成Writer(字符流)

  • 当处理纯文本数据时,如果使用字符流效率更高,并且可以有效解决中文问题,所以建议将字节流转换成字符流

  • 可以在使用时指定编码格式(比如utf-8, gbk , gb2312, ISO8859-1等)

/**
 * 使用 InputStreamReader 转换流解决中文乱码问题
 */
public static void main(String[] args) throws Exception {
    String filePath = ".\\2.txt";
    // 将 FileInputStream 转换成 InputStreamReader
    InputStreamReader isr = new InputStreamReader(new FileInputStream(filePath), "gbk");
    // 把 InputStreamReader 传入 BufferedReader
    BufferedReader br = new BufferedReader(isr);
    // 读取
    String s = br.readLine();
    System.out.println(s);
    // 关闭外层流
    br.close();
}

13.8 打印流

打印流只有输出流,没有输入流

13.8.1 PrintStream

PrintStream是OutputStream的子类,属于字节流

public static void main(String[] args) throws Exception{
    PrintStream out = System.out;
    // 默认情况下,PrintStream输出数据的位置是标准输出,即显示器
    out.print("hello, world");
    // out.write("hello, world".getBytes());
    // 修改打印流输出的位置/设备
    System.setOut(new PrintStream(".\\2.txt"));
    System.out.println("hello, world");
}

13.8.2 PrintWriter

PrintWriter是Writer的子类,属于字符流

public static void main(String[] args) throws Exception {
    // PrintWriter printWriter = new PrintWriter(System.out);
    PrintWriter printWriter = new PrintWriter(".\\2.txt");
    printWriter.print("北京你好");
    // close关闭并写入数据
    printWriter.close();
}

13.9 Properties 类

Properties是hashtable的子类,是专门用于读写配置文件的集合类,其中,配置文件的格式为

键=值
键=值

键值对不需要有空格,值不需要用引号引起来,默认类型是String

Properties的常用方法有

// load: 加载配置文件的键值对到Properties对象list:将数据显示到指定设备
// list: 将数据显示到指定设备
// getProperty(key): 根据键获取值
// setProperty(key, val): 设置键值对到Properties对象
// store: 将Properties中的键值对存储到配置文件,在idea中,保存信息到配置文件,如果含有中文,会存储为unicode码

例子

public static void main(String[] args) throws Exception{
    Properties properties = new Properties();
    properties.load(new FileReader(".\\mysql.properties"));
    properties.list(System.out);
    System.out.println(properties.getProperty("user"));
    System.out.println(properties.getProperty("pwd"));
}

14 网络编程

14.1 网络的相关概念

网络即两台设备之间通过网络实现数据传输

网络通信:将数据通过网络从一台设备传输到另一台设备

java.net包下提供了一系列的类或接口,供程序员使用,完成网络通信

其他的网络相关概念见计算机网络

14.2 InetAddress类

相关方法有

getLocalHost(); // 获取本机InetAddress对象
getByName(Host); // 根据指定主机名/域名获取ip地址对象
getHostName(); // 获取lnetAddress对象的主机名
getHostAddress(); //获取InetAddress对象的地址

例如

//获取本机InetAddress 对象getLocalHost
InetAddress localHost = InetAddress.getLocalHost();
System.out.println(localHost);
//根据指定主机名/域名获取ip 地址对象getByName
InetAddress host2 = InetAddress.getByName("ThinkPad-PC");
System.out.println(host2);
InetAddress host3 = InetAddress.getByName("www.baidu.com");
System.out.println(host3);
//获取InetAddress 对象的主机名getHostName
String host3Name = host3.getHostName();
System.out.println(host3Name);
//获取InetAddress 对象的地址getHostAddress
String host3Address = host3.getHostAddress();
System.out.println(host3Address);

14.3 Socket

套接字(Socket)开发网络应用程序被广泛采用,以至于成为事实上的标准。通信的两端都要有Socket,是两台机器间通信的端点网络通信其实就是Socket间的通信。

Socket允许程序把网络连接当成一个流,数据在两个Socket间通过IO传输,一般主动发起通信的应用程序属客户端,等待通信请求的为服务端

当我们需要通讯时(读写数据)

  • socket.getOutputStream()

  • socket.getInputStream()

14.4 TCP网络通信编程

  • 基于客户端—服务端的网络通信

  • 底层使用的是TCP/IP协议

  • 基于Socket的TCP编程

应用场景举例:客户端发送数据,服务端接受并显示控制台

客户端

public class SocketTCP01Server {
    public static void main(String[] args) throws IOException {
        // 在本机的9999端口监听,等待链接
        ServerSocket serverSocket = new ServerSocket(9999);
        // 阻塞等待,如果有客户端监听,会返回一个Socket对象
        Socket socket = serverSocket.accept();
        System.out.println("服务端 Socket = " + socket);
        // 读取客户端写入通道的数据
        InputStream inputStream = socket.getInputStream();
        byte[] buf = new byte[1024];
        int readLen;
        while ((readLen = inputStream.read(buf)) != -1){
            System.out.println(new String(buf, 0, readLen));
        }
        // 关闭流和socket
        inputStream.close();
        socket.close();
        System.out.println("服务端退出");
    }
}

服务端

public class SocketTCP01Client {
    public static void main(String[] args) throws IOException {
        // 连接本机的9999端口,如果连接成功,返回Socket对象
        Socket socket = new Socket(InetAddress.getLocalHost(), 9999);
        System.out.println("客户端 Socket = " + socket);
        // 得到和socket关联的输出流对象
        OutputStream outputStream = socket.getOutputStream();
        outputStream.write("hello, server".getBytes());
        // 设置一个结束标记
        socket.shutdownOutput();
        // 关闭流对象和socket
        outputStream.close();
        socket.close();
        System.out.println("客户端退出");
    }
}

如果 采用字符流,可以采用转换流,输入后需要手动刷新bufferedWriter.flush(),结束标记可以采用writer.newLine(),要求对方采用readLine()来读取

14.5 UDP网络通信编程

  • 类 DatagramSocket 和 DatagramPacket[数据包/数据报]实现了基于UDp协议网络程序。

  • UDP数据报通过数据报套接字DatagramSocket发送和接收,系统不保证UDP数据报一定能够安全送到目的地,也不能确定什么时候可以抵达。

  • DatagramPacket 对象封装了UDP数据报,在数据报中包含了发送端的IP地址和端口号以及接收端的IP地址和端口号。

  • UDP协议中每个数据报都给出了完整的地址信息,因此无须建立发送方和接收方的连接

数据接收端

public class UDPReceiver {
    public static void main(String[] args) throws IOException {
        // 创建一个DatagramSocket对象
        DatagramSocket datagramSocket = new DatagramSocket(9999);
        // UDP一个数据包最大64K
        byte[] buf = new byte[1024];
        // 接收数据, 获得数据报
        DatagramPacket datagramPacket = new DatagramPacket(buf, buf.length);
        datagramSocket.receive(datagramPacket);
        // 拆包,取出数据
        int length = datagramPacket.getLength();    // 实际接收到的数据长度
        byte[] data = datagramPacket.getData();     // 接收到的数据
        String s = new String(data, 0, length);
        System.out.println(s);
    }
}

数据发送端

public class UDPSender {
    public static void main(String[] args) throws IOException {
        // 创建DatagramSocket对象
        DatagramSocket datagramSocket = new DatagramSocket(8888);
        byte[] data = "你好啊~~~".getBytes();
        // 将需要发送的数据封装到DatagramPacket对象
        // DatagramPacket(字节数组, 数组长度, 主机IP, 端口)
        DatagramPacket datagramPacket =
                new DatagramPacket(data, data.length, InetAddress.getByName("192.168.110.1"), 9999);
        datagramSocket.send(datagramPacket);
        datagramSocket.close();
    }
}

发送端和接收端角色可以相互转化

15 反射

15.1 反射引入

  • 反射机制允许程序在执行期借助于Reflection API取得任何类的内部信息(比如成员变量,构造器,成员方法等等),并能操作对象的属性及方法。反射在设计模式和框架底层都会用到

  • 加载完类之后,在堆中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象包含了类的完整结构信息。通过这个对象得到类的结构。这个Class对象就像一面镜子,透过这个镜子看到类的结构,所以,形象的称之为:反射

public class ReflectionQuestion {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        /* 传统方法
         Cat cat = new Cat();
         cat.hi();
         */
        Properties properties = new Properties();
        properties.load(new FileInputStream(".\\re.properties"));
        String classfullpath = properties.get("classfullpath").toString();
        String method = properties.get("method").toString();
        System.out.println(classfullpath + method);
        // 加载类,返回Class类型的对象
        Class<?> aClass = Class.forName(classfullpath);
        // 得到类的运行对象实例
        Object o = aClass.newInstance();
        // 得到方法对象
        Method method1 = aClass.getMethod(method);
        // 通过method1调用方法
        method1.invoke(o);
    }
}

反射机制可以

  • 在运行时判断任意一个对象所属的类

  • 在运行时构造任意一个类的对象

  • 在运行时得到任意一个类所具有的成员变量和方法

  • 在运行时调用任意一个对象的成员变量和方法

  • 生成动态代理

优点:可以动态的创建和使用对象(也是框架底层核心).使用灵活,没有反射机制,框架技术就失去底层支撑。

缺点:使用反射基本是解释执行,对执行速度有影响.

15.2 反射相关的主要类

  • java.lang.Class:代表一个类,Class对象表示某个类加载后在堆中的对象

  • java.lang.reflect.Method: 代表类的方法,Method对象表示某个类的方法

  • java.lang.reflect.Field:代表类的成员变量, Field对象表示某个类的成员变量

  • java.lang.reflect.Constructor:代表类的构造方法,Constructor对象表示构造器

15.2.1 Class类

  • Class也是类,因此也继承Object类

  • Class类对象不是new出来的,而是系统创建的

  • 对于某个类的Class类对象,在内存中只有一份,因为类只加载一次

  • 每个类的实例都会记得自己是由哪个Class实例所生成

  • 通过Class对象可以完整地得到一个类的完整结构,通过一系列API

  • Class对象是存放在堆的

  • 类的字节码二进制数据,是放在方法区的,有的地方称为类的元数据(包括方法代码,变量名,方法名,访问权限等等)

Class 类的常用方法有

static Class forName(String name);  // 返回指定类名的Class对象
Object newInstance();       //调用缺省构造函数,返回该Class对象的一个实例
getName();          // 返回此Class对象所表示的实体(类、接口、数组类、基本类型等)名称
Class[] getInterfaces();    // 获取当前Class对象的接口
ClassLoader getClassLoader();   // 返回该类的类加载器
Class getSuperclass();      // 返回表示此Class所表示的实体的超类的Class
Constructor[] getConstructors(); // 返回一个包含某些Constructor对象的数组
Field[] getDeclaredFields();    // 返回Field对象的一个数组
Method getMethod(String name, Class ... , paramTypes)       // 返回一个Method对象,此对象的形参类型为paramType

获取Class对象的方式有

// Class.forName
String classAllPath = "com.lbl.Car"; //通过读取配置文件获取
Class<?> cls1 = Class.forName(classAllPath);
System.out.println(cls1);
​
// 类名.class, 用于参数传递
Class cls2 = Car.class;
System.out.println(cls2);
​
// 对象.getClass(), 用于有对象实例
Car car = new Car();
Class cls3 = car.getClass();
System.out.println(cls3);
​
// 通过类加载器来获取到类的Class对象
ClassLoader classLoader = car.getClass().getClassLoader();
Class cls4 = classLoader.loadClass(classAllPath);
System.out.println(cls4);
​
// 基本数据(int, char,boolean,float,double,byte,long,short) 按如下方式得到Class 类对象
Class<Integer> integerClass = int.class;
Class<Character> characterClass = char.class;
Class<Boolean> booleanClass = boolean.class;
System.out.println(integerClass);//int
​
// 基本数据类型对应的包装类,可以通过.TYPE 得到Class 类对象
Class<Integer> type1 = Integer.TYPE;
Class<Character> type2 = Character.TYPE;
System.out.println(type1);

15.2.2 Field 类

getModifiers();     // 以int形式返回修饰符,默认修饰符是0,public是1,private是2,protected是4,static是8,final是16
getType();      // 以Class形式返回类型
getName();      // 返回属性名

15.2.3 Method 类

getModifiers();     // 以int形式返回修饰符
getReturnType();    // 以Class形式获取返回类型
getName();          // 返回方法名
getParameterTypes();    // 以Class[]返回参数类型数组

15.2.4 Constructor 类

getModifiers();     // 以int形式返回修饰符
getName();          // 返回构造器名(全类名)
getParameterTypes();    //以Class[]返回参数类型数组

15.3 反射的优化

优点:可以动态的创建和使用对象(也是框架底层核心).使用灵活,没有反射机制,框架技术就失去底层支撑。

缺点:使用反射基本是解释执行,对执行速度有影响.

反射调用优化-关闭访问检查

  • Method和Field、Constructor对象都有setAccessible0方法

  • setAccessible作用是启动和禁用访问安全检查的开关

  • 参数值为true表示反射的对象在使用时取消访问检查,提高反射的效率。参数值为false则表示反射的对象执行访问检查

@SuppressWarnings("all")
public class Reflection01 {
    public static void main(String[] args) throws Exception {
        m1();   // 1182
        m2();   // 3
        m3();   // 2166
    }
​
    public static void m1() throws Exception{
        Class<?> aClass = Class.forName("com.lbl.reflection.Cat");
        Object o = aClass.newInstance();
        Method hi = aClass.getMethod("hi");
        hi.setAccessible(true);     // 在放射方法时,取消访问检查
        long start = System.currentTimeMillis();
        for (int i = 0; i < 1000000000; i++) {
            hi.invoke(o);
        }
        long end = System.currentTimeMillis();
        System.out.println(end - start);    // 1208
    }
​
    public static void m2(){
        Cat cat = new Cat();
        long start = System.currentTimeMillis();
        for (int i = 0; i < 1000000000; i++) {
            cat.hi();
        }
        long end = System.currentTimeMillis();
        System.out.println(end - start);    // 3
    }
​
    public static void m3() throws Exception{
        Class<?> aClass = Class.forName("com.lbl.reflection.Cat");
        Object o = aClass.newInstance();
        Method hi = aClass.getMethod("hi");
        long start = System.currentTimeMillis();
        for (int i = 0; i < 1000000000; i++) {
            hi.invoke(o);
        }
        long end = System.currentTimeMillis();
        System.out.println(end - start);    // 2166
    }
}

15.4 类加载

反射机制是java实现动态语言的关键,也就是通过反射实现类动态加载。

  • 静态加载:编译时加载相关的类,如果没有则报错,依赖性太强

  • 动态加载:运行时加载需要的类,如果运行时不用该类,即使不存在该类,则不报错,降低了依赖性

类加载时机

  • 当创建对象时(new) //静态加载

  • 当子类被加载时,父类也加载 //静态加载

  • 调用类中的静态成员时 //静态加载

  • 通过反射 //动态加载

15.4.1 加载阶段(Loading)

将类的class文件读入内存,并为之创建一个java.lang.Class对象。此过程由类加载器完成

JVM在该阶段的主要目的是将字节码从不同的数据源(可能是class文件、也可能是jar包,甚至网络)转化为二进制字节流加载到内存中,并生成一个代表该类的java.lang.Class对象

15.4.2 连接阶段(Linking)

将类的二进制数据合并到JRE中

15.4.2.1 验证(Verification)

  • 目的是为了确保Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

  • 包括:文件格式验证(是否以魔数oxcafebabe开头)、元数据验证、字节码验证和符号引用验证

  • 可以考虑使用-Xverify:none 参数来关闭大部分的类验证措施,缩短虚拟机类加载的时间。

15.4.2.2 准备(Preparation)

JVM 会在该阶段对静态变量,分配内存并默认初始化(对应数据类型的默认初始值,如0、OL、null、false等)。这些变量所使用的内存都将在方法区中进行分配

15.4.2.3 解析(Resolution)

虚拟机将常量池内的符号引用替换为直接引用的过程。

15.4.3 初始化(Initialization)

  • 到初始化阶段,才真正开始执行类中定义的Java程序代码,此阶段是执行<clinit>()方法的过程。

  • <clinit>()方法是由编译器按语句在源文件中出现的顺序,依次自动收集类中的所有静态变量的赋值动作和静态代码块中的语句,并进行合并。

  • 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕

15.5 通过反射操作类

15.5.1 通过反射创建对象

// 先获取到User 类的Class 对象
Class<?> userClass = Class.forName("com.lbl.reflection.User");
​
// 通过public 的无参构造器创建实例
Object o = userClass.newInstance();
System.out.println(o);
​
// 通过public 的有参构造器创建实例
Constructor<?> constructor = userClass.getConstructor(String.class);    // 先得到对应构造器
Object abc = constructor.newInstance("abc");    // 创建实例,并传入实参
System.out.println(abc);
​
// 通过非public 的有参构造器创建实例
Constructor<?> constructor1 = userClass.getDeclaredConstructor(int.class, String.class);    // 得到private 的构造器对象
constructor1.setAccessible(true);   // 暴破
Object user2 = constructor1.newInstance(100, "张三丰");
System.out.println(user2);
class User { //User 类
    private int age = 10;
    private String name = "小王";
    
    public User() {//无参public
    }
    
    public User(String name) {//public 的有参构造器
        this.name = name;
    }
    
    private User(int age, String name) {//private 有参构造器
        this.age = age;
        this.name = name;
    }
    
    public String toString() {
        return "User [age=" + age + ", name=" + name + "]";
    }
}

15.5.2 通过反射访问类中的成员

Class<?> stuClass = Class.forName("com.lbl.reflection.Student");
Object o = stuClass.newInstance();
​
// 使用反射得到age 属性对象
Field age = stuClass.getField("age");
// 通过反射来操作属性
age.set(o, 88);
// 返回age 属性的值
System.out.println(age.get(o));
​
// 使用反射操作name 属性
Field name = stuClass.getDeclaredField("name");
// 对name 进行暴破, 可以操作private 属性
name.setAccessible(true);
name.set(o, "张三");
class Student {//类
    public int age;
    private static String name;
    
    public Student() {//构造器
    }
    
    public String toString() {
        return "Student [age=" + age + ", name=" + name + "]";
    }
}

15.5.3 访问方法

Class<?> bossCls = Class.forName("com.lbl.reflection.Boss");
Object o = bossCls.newInstance();
​
// 调用public的hi方法
// Method hi = bossCls.getMethod("hi", String.class);
// 得到hi方法对象
Method hi = bossCls.getDeclaredMethod("hi", String.class);
// 调用hi方法
hi.invoke(o, "老王");
​
// 调用private static方法
// 得到say方法对象
Method say = bossCls.getDeclaredMethod("say", int.class, String.class, char.class);
say.setAccessible(true);    // 暴破
System.out.println(say.invoke(o, 100, "张三", '男'));
class Boss {//类
    public int age;
    private static String name;
    
    public Boss() {//构造器
    }
    
    public Monster m1() {
        return new Monster();
    }
    
    private static String say(int n, String s, char c) {//静态方法
        return n + " " + s + " " + c;
    }
    
    public void hi(String s) {//普通public 方法
        System.out.println("hi " + s);
    }
}