Java 学习笔记(二)

Java 学习笔记(二)


集合进阶

Collection

集合类的特点:提供一种存储空间可变的存储模型,存储的数据容量可以随时发生改变

Collection集合概述和使用

  • 是单列集合的顶层接口,他表示一组对象,这些对象也称为Collection的元素
  • JDK不提供此接口的任何直接实现,它提供更具体的子接口(如Set和List)实现

创建Collection集合的对象:

  • 多态的方式
  • 具体的实现类ArrayList
//创建Collection集合的对象
Collection<String> c = new ArrayList<String>();

//添加元素:boolean add(E e)
c.add("hello");
c.add("world");
c.add("java");

//输出集合对象
System.out.println(c);

Collection集合常用方法

方法名描述
boolean add(E e)添加元素
boolean remove(Object o)从集合中移除指定的元素
void clear()清空集合中的元素
boolean contains(Object o)判断集合中是否存在指定的元素
boolean isEmpty()判断集合是否为空
int size()集合的长度,也就是集合中元素的个数
//创建Collection集合对象
Collection<String> c = new ArrayList<String>();

//boolean add(E e) 添加元素
//System.out.println(c.add("hello"));
//System.out.println(c.add("world"));
//System.out.println(c.add("world"));
c.add("hello");
c.add("world");
c.add("java");


//boolean remove(Object o) 从集合中移除指定的元素
System.out.println(c.remove("world"));
System.out.println(c.remove("javaee"));

//void clear() 清空集合中的元素
//c.clear();

//boolean contains(Object o) 判断集合中是否存在指定的元素
System.out.println(c.contains("world"));
System.out.println(c.contains("javaee"));

//boolean isEmpty() 判断集合是否为空
System.out.println(c.isEmpty());

//int size() 集合的长度,也就是集合中元素的个数
System.out.println(c.size());

//输出集合对象
System.out.println(c);

Collection集合的遍历

  • Iterator:迭代器,集合的专用遍历方式
  • Iterator<E> iterator():返回此合集中元素的迭代器,通过集合的iterator()方法得到
  • 迭代器是通过集合的iterator()方法得到的,所以我们说它是依赖于集合而存在的

Iterator中的常用方法:

方法名描述
E next()返回迭代中的下一个元素
boolean hasNext()如果迭代具有更多元素,则返回true
Collection<String> c = new ArrayList<String>();

c.add("hello");
c.add("world");
c.add("java");

//Iterator<E> iterator():返回此合集中元素的迭代器,通过集合的iterator()方法得到
Iterator<String> it = c.iterator();

//E next() 返回迭代中的下一个元素
/*System.out.println(it.next());
System.out.println(it.next());
System.out.println(it.next());*/

//boolean hasNext() 如果迭代具有更多元素,则返回true
/*if (it.hasNext()) {
    System.out.println(it.next());
}
if (it.hasNext()) {
    System.out.println(it.next());
}
if (it.hasNext()) {
    System.out.println(it.next());
}
if (it.hasNext()) {
    System.out.println(it.next());
}*/

//用while循环改进判断
while (it.hasNext()) {
//  System.out.println(it.next());
    String s = it.next();
    System.out.println(s);
}

Collection集合存储学生对象并遍历

需求:创建一个存储学生对象的集合,存储3个学生对象,使用程序实现在控制台遍历该集合。

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

public class Demo {
    public static void main(String[] args) {
        //创建Collection集合对象
        Collection<Student> c = new ArrayList<Student>();

        //创建学生对象
        Student s1 = new Student("林青霞", 30);
        Student s2 = new Student("张曼玉", 35);
        Student s3 = new Student("王祖贤", 33);

        //把学生添加到集合
        c.add(s1);
        c.add(s2);
        c.add(s3);

        //遍历集合(迭代器方式)
        Iterator<Student> it = c.iterator();
        while (it.hasNext()) {
            Student s = it.next();
            System.out.println(s.getName() + "," + s.getAge());
        }
    }
}

List

List集合概述

  • 有序集合(也称为序列),用户可以精确控制列表中每个元素的插入位置。用户可以通过整数索引访问元素,并搜索列表中的元素。
  • 与Set集合不同,列表通常允许重复的元素

List集合特点:

  • 有序:存储和取出的元素顺序一致
  • 可重复:存储的元素可以重复
//创建集合对象
List<String> list = new ArrayList<String>();

//添加元素
list.add("hello");
list.add("world");
list.add("java");
list.add("world");

//输出集合对象
//System.out.println(list);

//遍历集合(迭代器)
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String s = it.next();
    System.out.println(s);
}

List集合的特有方法

方法名描述
void add(int index, E element)在此集合中的指定位置插入指定的元素
E remove(int index)删除指定索引处的元素,返回被删除的元素
E set(int index, E element)修改指定索引处的元素,返回被修改的元素
E get(int index)返回指定索引处的元素
List<String> list = new ArrayList<String>();

list.add("hello");
list.add("world");
list.add("java");

//void add(int index, E element) 在此集合中的指定位置插入指定的元素
list.add(1, "javaee");
//list.add(11, "javaee");  //IndexOutOfBoundsException  索引越界

//E remove(int index) 删除指定索引处的元素,返回被删除的元素
System.out.println(list.remove(1));

//E set(int index, E element) 修改指定索引处的元素,返回被修改的元素
System.out.println(list.set(1, "javaee"));

//E get(int index)  返回指定索引处的元素
System.out.println(list.get(1));

System.out.println(list);

//用for循环遍历集合
for (int i = 0; i < list.size(); i++) {
    String s = list.get(i);
    System.out.println(s);
}

案例:List集合存储学生对象并遍历

List<Student> list = new ArrayList<Student>();

Student s1 = new Student("林青霞", 30);
Student s2 = new Student("张曼玉", 35);
Student s3 = new Student("王祖贤", 33);

list.add(s1);
list.add(s2);
list.add(s3);


//for循环的方式遍历
for (int i = 0; i < list.size(); i++) {
    Student s = list.get(i);
    System.out.println(s.getName() + "," + s.getAge());
}

System.out.println("--------");

//迭代器的方式遍历
Iterator<Student> it = list.iterator();
while (it.hasNext()) {
    Student s = it.next();
    System.out.println(s.getName() + "," + s.getAge());
}

并发修改异常

并发修改异常 —— ConcurrentModificationException

产生原因:

迭代器遍历的过程中,通过集合对象修改了集合中元素的长度,造成了迭代器获取元素中判断预期修改值与实际修改值不一致

解决方案

用for循环遍历,然后用集合对象做对应的操作即可

List<String> list = new ArrayList<String>();

list.add("hello");
list.add("world");
list.add("java");

/*Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String s = it.next();
    if (s.equals("world")) {
        list.add("javaee");
    }
}*/  //ConcurrentModificationException  当不允许这样的修改时,可以通过检测到对象的并发修改的方法来抛出此异常

for (int i = 0; i < list.size(); i++) {
    String s = list.get(i);
    if (s.equals("world")) {
        list.add("javaee");
    }
}

System.out.println(list);

列表迭代器

  • ListIterator —— 列表迭代器
  • 通过List集合的listIterator()方法得到,所以说它是List集合特有的迭代器
  • 用于允许程序员沿任一方向遍历列表的列表的迭代器,在迭代期间修改列表,并获取列表中迭代器的当前位置

常用方法:

方法名描述
E next()返回列表中的下一个元素,并且前进光标位置
boolean hasNext()如果此列表迭代器在向前方向遍历列表时具有更多元素,则返回 true
E previous()返回列表中的上一个元素,并向后移动光标位置
boolean hasPrevious()如果此列表迭代器在相反方向遍历列表时具有更多元素,则返回 true
void add(E e)将指定的元素插入列表(可选操作)
List<String> list = new ArrayList<>();

list.add("hello");
list.add("world");
list.add("java");

ListIterator<String> lit = list.listIterator();
while (lit.hasNext()) {
    String s = lit.next();
    System.out.println(s);
}

System.out.println("--------");

//逆向遍历
while (lit.hasPrevious()) {
    String s = lit.previous();
    System.out.println(s);
}

System.out.println("--------");

//获取列表迭代器
while (lit.hasNext()) {
    String s = lit.next();
    if (s.equals("world")) {
        lit.add("javaee");
    }
}

System.out.println(list);

增强for循环

  • 增强for:简化数组和Collection集合的遍历
  • 实现此接口允许对象成为增强型 for语句的目标
  • 它是JDK5之后出现的,其内部原理是一个Iterator迭代器

格式:

for (元素数据类型 变量名 : 数组或者Collection集合) {
    //在此处使用变量即可,该变量就是元素
}
int[] arr = {1, 2, 3, 4, 5};
for (int i : arr) {
    System.out.println(i);
}

System.out.println("--------");

String[] str = {"hello", "world", "java"};
for (String s : str) {
    System.out.println(s);
}

System.out.println("--------");

List<String> list = new ArrayList<String>();
list.add("hello");
list.add("world");
list.add("java");

for (String s : list) {
    System.out.println(s);
}

System.out.println("--------");

//内部原理是一个Iterator迭代器
for (String s : list) {
    if (s.equals("world")) {
        list.add("javaee"); //ConcurrentModificationException
    }
}

案例:List集合存储学生对象用三种方式遍历

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class Demo {
    public static void main(String[] args) {
        List<Student> list = new ArrayList<Student>();
        Student s1 = new Student("林青霞", 30);
        Student s2 = new Student("张曼玉", 35);
        Student s3 = new Student("王祖贤", 33);

        list.add(s1);
        list.add(s2);
        list.add(s3);

        //迭代器方式,集合特有的遍历方式
        Iterator<Student> it = list.iterator();
        while (it.hasNext()) {
            Student s = it.next();
            System.out.println(s.getName() + "," + s.getAge());
        }

        System.out.println("--------");

        //普通for循环方式
        for (int i = 0; i < list.size(); i++) {
            Student s = list.get(i);
            System.out.println(s.getName() + "," + s.getAge());
        }

        System.out.println("--------");

        //增强for循环
        for (Student s : list) {
            System.out.println(s.getName() + "," + s.getAge());
        }
    }
}

数据结构

栈和队列
  • 数据结构是计算机存储、组织数据的方式,是指相互之间存在一种或多种特定关系的数组元素的集合。
  • 通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。

栈:

  • 数据进入栈模型的过程称为:压/进栈
  • 数据离开栈模型的过程称为:弹/出栈
  • 栈是一种数据先进后出的模型

队列:

  • 数据从后端进入队列模型的过程称为:入队列
  • 数据从前端进出队列模型的过程称为:出队列
  • 队列是一种数据先进先出的模型
数组和链表

数组:

  • 查询数据通过索引定位,查询任意数据耗时相同,查询速度快
  • 删除数据时,要将原始数据删除,同时后面每个数据前移,删除效率低
  • 添加数据时,添加位置后的每个数据后移,再添加元素,添加效率极低

链表:

  • 链表的每个元素称为结点,一个结点包含:结点的存数位置(地址)、存储具体的数据、下一个结点的地址
  • 链表是一种增删快的模型(对比数组)
  • 链表是一种查询慢的模型(对比数组)

List集合子类特点

  • List集合的常用子类:ArrayList、LinkedList
  • ArrayList:底层数据结构是数组,查询快,增删慢
  • LinkedList:底层数据结构是链表,查询慢,增删快
//需求:分别使用ArrayList和LinkedList完成存储字符串并遍历
//创建集合对象
ArrayList<String> array = new ArrayList<String>();

array.add("hello");
array.add("world");
array.add("java");

//遍历
for (String s : array) {
    System.out.println(s);
}

System.out.println("--------");

LinkedList<String> linkedlist = new LinkedList<String>();

linkedlist.add("hello");
linkedlist.add("world");
linkedlist.add("java");

for (String s : linkedlist) {
    System.out.println(s);
}

案例:ArrayList集合存储学生对象用三种方式遍历

import java.util.ArrayList;
import java.util.Iterator;

public class Demo {
    public static void main(String[] args) {
        ArrayList<Student> array = new ArrayList<Student>();

        Student s1 = new Student("林青霞", 30);
        Student s2 = new Student("张曼玉", 35);
        Student s3 = new Student("王祖贤", 33);

        array.add(s1);
        array.add(s2);
        array.add(s3);

        //迭代器
        Iterator<Student> it = array.iterator();
        while (it.hasNext()) {
            Student s = it.next();
            System.out.println(s.getName() + "," + s.getAge());
        }

        System.out.println("--------");

        //for
        for (int i = 0; i < array.size(); i++) {
            Student s = array.get(i);
            System.out.println(s.getName() + "," + s.getAge());
        }

        System.out.println("--------");

        //增强for
        for (Student s : array) {
            System.out.println(s.getName() + "," + s.getAge());
        }
    }
}

LinkedList集合的特有功能

方法名描述
public void addFirst(E e)在该列表开头插入指定的元素
public void addLast(E e)将指定的元素追加到此列表的末尾
public E getFirst()返回此列表中的第一个元素
public E getLast()返回此列表中的最后一个元素
public E removeFirst()从此列表中删除并返回第一个元素
public E removeLast()从此列表中删除并返回最后一个元素
LinkedList<String> linkedList = new LinkedList<String>();

linkedList.add("hello");
linkedList.add("world");
linkedList.add("java");

linkedList.addFirst("javaee");
linkedList.addLast("javaee");

System.out.println("--------");

System.out.println(linkedList.getFirst());
System.out.println(linkedList.getLast());
System.out.println(linkedList);

System.out.println("--------");

System.out.println(linkedList.removeFirst());
System.out.println(linkedList.removeLast());
System.out.println(linkedList);

Set

Set集合概述和特点

  • 不包含重复元素的集合
  • 没有带索引的方法,所以不能使用普通for循环遍历
  • HashSet对集合的迭代顺序不做任何保证
//用Set集合存储字符串并遍历
Set<String> s = new HashSet<String>();

s.add("hello");
s.add("world");
s.add("java");

Iterator<String> it = s.iterator();
while (it.hasNext()) {
    String s1 = it.next();
    System.out.println(s1);
}

System.out.println("--------");

for (String s1 : s) {
    System.out.println(s1);
}

哈希值

  • 哈希值是JDK根据对象的地址或者字符串或者数字算出来的int类型的数值
  • Object类中有一个方法可以获取对象的哈希值
    • public int hashCode():返回对象的哈希码值

对象哈希值的特点:

  • 同一个对象多次调用hashCode()方法返回第哈希值是相同的
  • 默认情况下,不同对象的哈希值是不同的,而重写hashCode()方法可以实现让不同对象的哈希值相同
Student s1 = new Student("林青霞", 30);

//同一对象多次调用hashCode()方法返回第哈希值是相同的
System.out.println(s1.hashCode());
System.out.println(s1.hashCode());

System.out.println("--------");

//默认情况下,不同对象的哈希值是不相同的
//通过方法重写可以实现不同对象的哈希值是相同的
Student s2 = new Student("林青霞", 30);
System.out.println(s2.hashCode());

System.out.println("--------");

System.out.println("hello".hashCode());  //99162322
System.out.println("world".hashCode());  //113318802
System.out.println("java".hashCode());  //3254818

System.out.println("--------");

// 字符串中重写了hashCode()方法
System.out.println("重地".hashCode());   //1179395
System.out.println("通话".hashCode());  //1179395

HashSet

HashSet集合概述和特点

HashSet集合特点

  • 底层数据结构是哈希表
  • 对集合的迭代顺序不作任何保证,也就是说不保证存储和取出的元素顺序一致。
  • 没有带索引的方法,索引不能使用普通for循环遍历
  • 由于是Set集合,索引是不包含重复元素的
//创建集合对象
HashSet<String> hs = new HashSet<String>();

// 添加元素
hs.add("hello");
hs.add("world");
hs.add("java");

// 遍历
for (String s : hs) {
    System.out.println(s);
}
HashSet集合保证元素唯一性源码分析

HashSet集合存储元素,要保证元素唯一性,需要重写hashCode()equals()方法。

HashSet 存储一个元素的过程:

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

public class Demo {
    public static void main(String[] args) {
        //用Set集合存储字符串并遍历
        Set<String> s = new HashSet<String>();

        s.add("hello");
        s.add("world");
        s.add("java");

        Iterator<String> it = s.iterator();
        while (it.hasNext()) {
            String s1 = it.next();
            System.out.println(s1);
        }

        System.out.println("--------");

        for (String s1 : s) {
            System.out.println(s1);
        }
    }
}
//用Set集合存储字符串并遍历
Set<String> s = new HashSet<String>();

s.add("hello");
s.add("world");
s.add("java");
---------------------------------------------------------------

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

//哈希值和元素的hashCode()方法相关
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;

//如果哈希表未进行初始化就对其进行初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;

//根据对象的哈希值计算对象的存储位置,如果该位置没有元素,就存储元素
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;

        /*
            存入的元素和以前的元素比较哈希值
                如果哈希值不同,会继续向下执行,把元素添加到集合
                如果哈希值相同,会调用对象的equals()方法比较
                    如果返回false,会继续向下执行,把元素添加到集合
                    如果返回true,说明元素重复,不存储
        */
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}
常见数据结构之哈希表

哈希表

  • JDK8之前,底层采用数组+链表实现,可以说是一个元素为链表的数组
  • JDK8之后,在长度比较长的时候,底层实现了优化
案例:HashSet集合存储学生对象并遍历
import java.util.HashSet;

public class Demo {
    public static void main(String[] args) {
        Student s1 = new Student("林青霞", 30);
        Student s2 = new Student("张曼玉", 35);
        Student s3 = new Student("王祖贤", 33);

        Student s4 = new Student("王祖贤", 33);

        HashSet<Student> hs = new HashSet<Student>();

        hs.add(s1);
        hs.add(s2);
        hs.add(s3);

        for (Student s : hs) {
            System.out.println(s.getName() + "," + s.getAge());
        }
    }
}

LinkedHashSet

LinkedHashSet集合概述和特点

LinkedHashSet结合特点

  • 哈希表和链表实现的Set接口,具有可预测的迭代次序
  • 由链表保证元素有序,也就是说元素的存储和取出顺序是一致的
  • 由哈希表保证元素唯一,也就是说没有重复的元素
LinkedHashSet<String> linkedHsahSet = new LinkedHashSet<String>();

linkedHsahSet.add("hello");
linkedHsahSet.add("world");
linkedHsahSet.add("java");

for (String s : linkedHsahSet) {
    System.out.println(s);
}

TreeSet

TreeSet集合概述和特点

TreeSet集合特点:

  • 元素有序,这里的顺序不是指存储和取出的顺序,而是按照一定的规律进行排序,具体排序方法取决于构造方法。
  • 没有带索引的方法,所以不能使用普通for循环遍历
  • 由于是Set集合,所以不包含重复元素

构造方法

构造方法描述
TreeSet()根据元素的自然排序进行排序
TreeSet(Comparator comparator)根据指定的比较器进行排序

TreeSet集合存储整数并遍历

TreeSet<Integer> ts = new TreeSet<Integer>();
ts.add(10);
ts.add(40);
ts.add(30);
ts.add(50);
ts.add(20);

ts.add(30);

for (Integer i : ts) {
    System.out.println(i); // 10 20 30 40 50
}
自然排序Comparable的使用
  • 用TreeSet集合存储自定义对象,无参构造方法使用的是自然排序对元素进行排序的
  • 自然排序,就是让元素所属的类实现Comparable接口,重写compareTo(To)方法
  • 重写方法时,一定要注意排序规则必须按照要求的主要条件和次要条件来写

需求:存储学生对象并遍历,创建TreeSet集合使用无参构造方法
要求:按照年龄从小到大进行排序,年龄相同时,按照姓名的字母顺序排序

public class Student implements Comparable<Student> {
    private String name;
    private int age;

    public Student() {
    }

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public int compareTo(Student s) {
//        return 0; 只添加一个
//        return 1; 升序添加
//        return -1; 降序添加
        //按照年龄从小到大进行排序
        int num = this.age - s.age;
        //年龄相同时,按照姓名的字母顺序排序
        int num2 = num == 0 ? this.name.compareTo(s.name) : num;
        return num2;
    }
}
TreeSet<Student> ts = new TreeSet<Student>();
Student s1 = new Student("xishi", 29);
Student s2 = new Student("wangzhaojun", 28);
Student s3 = new Student("diaochan", 30);
Student s4 = new Student("yangyuhuan", 33);

Student s5 = new Student("linqingxia", 33);
Student s6 = new Student("linqingxia", 33);

ts.add(s1);
ts.add(s2);
ts.add(s3);
ts.add(s4);
ts.add(s5);
ts.add(s6);

for (Student s : ts) {
    System.out.println(s.getName() + "," + s.getAge());
}
比较器排序Comparator的使用
  • 用TreeSet集合存储自定义对象,带参构造方法使用的是比较器排序对元素进行排序的
  • 比较器排序,就是让集合构造方法接收Comparator的实现类对象,重写compare(To 1, To 2)方法
  • 重写方法时,一定要注意排序规则必须按照要求的主要条件和次要条件来写

存储学生对象并遍历,创建TreeSet集合使用带参构造方法
要求:按照年龄从小到大进行排序,年龄相同时,按照行姓名的字母顺序排序

import java.util.Comparator;
import java.util.TreeSet;

public class Demo {
    public static void main(String[] args) {
        TreeSet<Student> ts = new TreeSet<Student>(new Comparator<Student>() {
            @Override
            public int compare(Student s1, Student s2) {
                int num = s1.getAge() - s2.getAge();
                int num2 = num == 0 ? s1.getName().compareTo(s2.getName()) : num;
                return num2;
            }
        });

        Student s1 = new Student("xishi", 29);
        Student s2 = new Student("wangzhaojun", 28);
        Student s3 = new Student("diaochan", 30);
        Student s4 = new Student("yangyuhuan", 33);

        Student s5 = new Student("linqingxia", 33);
        Student s6 = new Student("linqingxia", 33);

        ts.add(s1);
        ts.add(s2);
        ts.add(s3);
        ts.add(s4);
        ts.add(s5);
        ts.add(s6);

        for (Student s : ts) {
            System.out.println(s.getName() + "," + s.getAge());
        }
    }
}

案例:成绩排序

需求:用TreeSet集合存储多个学生信息(姓名、语文成绩、数学成绩),并遍历该集合
要求:按照总分从高到低出现

public class Student {
    private String name;
    private int chinese;
    private int math;

    public Student() {
    }

    public Student(String name, int chinese, int math) {
        this.name = name;
        this.chinese = chinese;
        this.math = math;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getChinese() {
        return chinese;
    }

    public void setChinese(int chinese) {
        this.chinese = chinese;
    }

    public int getMath() {
        return math;
    }

    public void setMath(int math) {
        this.math = math;
    }

    public int getSum() {
        return this.chinese + this.math;
    }
}
import java.util.Comparator;
import java.util.TreeSet;

public class Demo {
    public static void main(String[] args) {
        TreeSet<Student> ts = new TreeSet<Student>(new Comparator<Student>() {
            @Override
            public int compare(Student s1, Student s2) {
                //主要条件
                int num = s2.getSum() - s1.getSum();
                //次要条件
                int num2 = num == 0 ? s1.getChinese() - s2.getChinese() : num;
                int num3 = num2 == 0 ? s1.getName().compareTo(s2.getName()) : num2;
                return num3;
            }
        });

        Student s1 = new Student("林青霞", 98, 100);
        Student s2 = new Student("张曼玉", 95, 95);
        Student s3 = new Student("王祖贤", 100, 93);
        Student s4 = new Student("柳岩", 100, 97);
        Student s5 = new Student("风清扬", 98, 98);

        Student s6 = new Student("左冷禅", 97, 99);
        Student s7 = new Student("赵云", 97, 99);

        ts.add(s1);
        ts.add(s2);
        ts.add(s3);
        ts.add(s4);
        ts.add(s5);
        ts.add(s6);
        ts.add(s7);

        for (Student s : ts) {
            System.out.println(s.getName() + "," + s.getChinese() + "," + s.getMath() + "," + s.getSum());
        }
    }
}
案例:不重复的随机数

要求:编写一个程序,获取10个1-20之间的随机数,要求随机数不能重复,并在控制台输出。

import java.util.Random;
import java.util.Set;
import java.util.TreeSet;

public class Demo {
    public static void main(String[] args) {
//      Set<Integer> set = new HashSet<Integer>();
        Set<Integer> set = new TreeSet<Integer>();

        Random r = new Random();

        while (set.size() < 10) {
            int number = r.nextInt(20) + 1;
            set.add(number);
        }

        for (Integer i : set) {
            System.out.println(i);
        }
    }
}

泛型

泛型概述

  • 泛型:是JDK中引入的特性,它提供了编译时类型安全检测机制,该机制允许在编译时检测到非法的类型。
  • 它的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数
  • 一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。
  • 参数化类型,顾名思义,就是将参数类型由原来的具体的类型参数化,然后在使用/调用时传入具体的类型
  • 这种参数类型可以用在类、方法和接口中,分别被称为泛型类、泛型方法、泛型接口

泛型定义格式:

  • <类型>:指定一种类型的格式。这里的类型可以看成是形参
  • <类型1,类型2,...>:指定多种类型的格式,多种类型之间用逗号隔开。这里的类型可以看成是形参
  • 将来具体调用的时候给定的类型可以看成是实参,并且实参的数据类型只能是引用数据类型

泛型的好处:

  • 把运行时期的问题提前到了编译期间
  • 避免了强制类型转换

泛型类

泛型类的定义格式:

  • 格式:修饰符 class 类名<类型> { }
  • 范例:public class Generic<T> { }
  • 此处的T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
public class Generic<T> {
    private T t;

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }
}
Generic<String> g1 = new Generic<String>();
g1.setT("林青霞");
System.out.println(g1.getT());

Generic<Integer> g2 = new Generic<Integer>();
g2.setT(30);
System.out.println(g2.getT());

泛型方法

泛型方法的定义格式:

  • 格式:
修饰符 <类型> 返回值类型 方法名(类型 变量名) {}
  • 范例:
public <T> void sjow(T t) {} 

public class Generic {
    public <T> void show(T t) {
        System.out.println(t);
    }
}
Generic g = new Generic();
g.show("林青霞");
g.show(30);
g.show(true);
g.show(12.34);

泛型接口

泛型接口的定义格式:

  • 格式:
修饰符 interface 接口名<类型> {}
  • 范例:
public interface Generic<T> {}

public interface Generic<t> {
    void show(T t);
}
public class GenericImpl<T> implements Generic<T> {
    @override
    public void show(T t) {
        System.out.println(t);
    }
}
Generic<String> g1 = new GenericImpl<String>();
g1.show("林青霞");

Generic<Integer> g2 = new GenericImpl<Integer>();
g2.show(30);

类型通配符

为了表示各种泛型List的父类,可以使用类型通配符。

  • 类型通配符:<?>
  • List<?>:表示元素类型未知的List,它的元素可以匹配任何的类型
  • 这种带通配符的List仅表示它是各种泛型List的父类,并不能把元素添加到其中

类型通配符上限

  • 如果不希望List<?>是任何泛型List的父类,只希望它代表某一类泛型的父类,可以使用类型通配符的上限
  • 类型通配符上限:<? extends 类型>
  • List<? extends Number>:它表示的类型是Number或者其子类型

类型通配符下限

  • 类型通配符下限:<? super 类型>
  • List<? super Number>:它表示的是Number或者其父类型

可变参数

可变参数又称参数个数可变,用作方法形参出现,那么方法参数个数就是可变的了。

  • 格式:
修饰符 返回值类型 方法名(数据类型... 变量名) {}
  • 范例:
public static int sum(int... a) {}

public static int sum(int... a) {
    int sum = 0;
    for (int i : a) {
        sum += i;
    }
    return sum;
}
System.out.println(sum(10, 20));
System.out.println(sum(10, 20, 30));
System.out.println(sum(10, 20, 30, 40));

可变参数注意事项:

  • 这里的变量其实是一个数组
  • 如果一个方法有多个参数,包含可变参数,可变参数要放在最后
public static int sum(int b, int... a) {}
可变参数的使用

Arrays工具类中有一个静态方法:

  • public static <T> List<T> asList(T... a):返回由指定数组支持的固定大小的列表
  • 返回的集合不能做增删操作,可以做修改操作

List接口中有一个静态方法:

  • public static <E> List<E> of(E... elements):返回包含任意数量元素的不可变列表
  • 返回的集合不能做增删改操作

Set接口中有一个静态方法:

  • public static <E> Set<E> of(E... elements):返回一个包含任意数量元素的不可变集合
  • 在给元素的时候,不能给重复的元素
  • 返回的集合不能做增删操作,没有修改的方法

Map

Map集合概述和特点

Map结合概述

  • Interface Map<K, V> K:键的类型; V:值的类型
  • 将键映射到值的对象;不能包含重复的键;每个键可以映射到最多一个值

创建Map集合的对象

  • 多态的方式
  • 具体的实现类HashMap
import java.util.HashMap;
import java.util.Map;

public class MapDemo {
    public static void main(String[] args) {
        // 创建集合对象
        Map<String, String> map = new HashMap<String, String>();

        // V put (K key, V value) 将指定的值与该映射中的指定键相关联
        map.put("itheima001", "林青霞");
        map.put("itheima002", "张曼玉");
        map.put("itheima003", "王祖贤");
        map.put("itheima003", "柳岩"); // 替换了王祖贤

        // 输出集合对象
        System.out.println(map);
    }
}

Map集合的基本功能

方法名描述
V put(K key, V value)添加元素
V remove(Object key)根据键删除键值对元素
void clear()移除所有的键值对元素
boolean containsKey(Object key)判断集合是否包含指定的键
boolean containsValue(Object value)判断集合是否包含指定的值
boolean isEmpty()判断集合是否为空
int size()集合的长度,也就是集合中键值对的个数

Map集合的获取功能

方法名描述
V get(Object key)根据键获取值
Set keySet()获取所有键的集合
Collection values()获取所有值的集合
Set<Map.Entry<K, V>> entrySet()获取所有键值对对象的集合

Map集合的遍历(方式一)

遍历思路:

  • 获取所有键的集合,用keySet()方法实现
  • 遍历键的集合,获取到每一个键,用增强for实现
  • 根据键去找值,用get(Object key)方法实现
// 创建集合对象
Map<String, String> map = new HashMap<String, String>();

// 添加元素
map.put("张无忌", "赵敏");
map.put("郭靖", "黄蓉");
map.put("杨过", "小龙女");

// 获取所有键的集合
Set<String> keySet = map.keySet();

// 遍历键的集合,获取到每一个键
for (String key : keySet) {
    // 根据键去找值
    String value = map.get(key);
    System.out.println(key + "," + value);
}

Map集合的遍历(方式二)

遍历思路:

  • 获取所有键值对对象的集合,用Set<Map.Entry<K, V>> entrySet()方法实现
  • 遍历键值对对象的集合,得到每一个键值对对象集合,用增强for实现
  • 根据键值对对象获取键和值,用getKey()getValue()方法实现
// 创建集合对象
Map<String, String> map = new HashMap<String, String>();

// 添加元素
map.put("张无忌", "赵敏");
map.put("郭靖", "黄蓉");
map.put("杨过", "小龙女");

// 获取所有键值对对象的集合
Set<Map.Entry<String, String>> entrySet = map.entrySet();

// 遍历键值对对象的集合,得到每一个键值对对象集合
for (Map.Entry<String, String> me : entrySet) {
    // 根据键值对对象获取键和值
    String key = me.getKey();
    String value = me.getValue();
    System.out.println(key + "," + value);
}

案例:HashMap集合存储学生对象并遍历

// 创建集合对象
Map<String, Student> hm = new HashMap<String, Student>();

// 创建学生对象
Student s1 = new Student("林青霞", 30);
Student s2 = new Student("张曼玉", 35);
Student s3 = new Student("王祖贤", 33);

// 添加元素
hm.put("itheima001", s1);
hm.put("itheima002", s2);
hm.put("itheima003", s3);

// 方式一:键找值
Set<String> keySet = hm.keySet();
for (String key : keySet) {
    Student value = hm.get(key);
    System.out.println(key + "," + value.getName() + "," + value.getAge());
}

System.out.println("--------");

// 方式二:键值对对象找键和值
Set<Map.Entry<String, Student>> entrySet = hm.entrySet();
for (Map.Entry<String, Student> me : entrySet) {
    String key = me.getKey();
    Student value = me.getValue();
    System.out.println(key + "," + value.getName() + "," + value.getAge());
}

案例:HashMap集合存储学生对象并遍历

需求:创建一个HashMap集合,键是学生对象(Student),值是居住地(String)。存储多个键值对元素,并遍历。
要求保证键的唯一性:如果学生对象的成员变量相同,我们就认为是同一个对象

import java.util.Objects;

public class Student {
    private String name;
    private int age;

    public Student() {
    }

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Student student = (Student) o;

        if (age != student.age) return false;
        return Objects.equals(name, student.name);
    }

    @Override
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age;
        return result;
    }
}
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class MapDemo {
    public static void main(String[] args) {
        // 创建集合对象
        Map<Student, String> hm = new HashMap<Student, String>();

        // 创建学生对象
        Student s1 = new Student("林青霞", 30);
        Student s2 = new Student("张曼玉", 35);
        Student s3 = new Student("王祖贤", 33);
        Student s4 = new Student("王祖贤", 33);

        // 添加元素
        hm.put(s1, "西安");
        hm.put(s2, "武汉");
        hm.put(s3, "郑州");
        hm.put(s4, "北京");

        // 方式一:键找值
        Set<Student> keySet = hm.keySet();
        for (Student key : keySet) {
            String value = hm.get(key);
            System.out.println(key.getName() + "," + key.getAge() + "," + value);
        }

        System.out.println("--------");

        // 方式二:键值对对象找键和值
        Set<Map.Entry<Student, String>> entrySet = hm.entrySet();
        for (Map.Entry<Student, String> me : entrySet) {
            Student key = me.getKey();
            String value = me.getValue();
            System.out.println(key.getName() + "," + key.getAge() + "," + value);
        }
    }
}

案例:ArrayList集合存储HashMap元素并遍历

需求:创建一个ArrayList集合,存储三个元素,每一个元素都是HashMap,每一个HashMap的键和值都是String,并遍历。

// 创建ArrayList集合
ArrayList<HashMap<String, String>> array = new ArrayList<HashMap<String, String>>();
// 创建HashMap集合并添加键值对元素
HashMap<String, String> hm1 = new HashMap<String, String>();
hm1.put("孙策", "大乔");
hm1.put("周瑜", "小乔");
// 把HashMap作为元素添加到ArrayList集合
array.add(hm1);

// 创建HashMap集合并添加键值对元素
HashMap<String, String> hm2 = new HashMap<String, String>();
hm1.put("郭靖", "黄蓉");
hm1.put("杨过", "小龙女");
// 把HashMap作为元素添加到ArrayList集合
array.add(hm2);

// 创建HashMap集合并添加键值对元素
HashMap<String, String> hm3 = new HashMap<String, String>();
hm1.put("令狐冲", "任盈盈");
hm1.put("林平之", "岳灵珊");
// 把HashMap作为元素添加到ArrayList集合
array.add(hm3);

// 遍历ArrayList集合
for (HashMap<String, String> hm : array) {
    Set<String> keySet = hm.keySet();
    for (String key : keySet) {
        String value = hm.get(key);
        System.out.println(key + "," + value);
    }
}

案例:HashMap集合存储ArrayList元素并遍历

需求:创建一个HashMap集合,存储三个键值对元素,每一个键值对元素的键是String,值是ArrayList,每一个ArrayList的元素是String,并遍历。

// 创建HashMap集合
HashMap<String, ArrayList<String>> hm = new HashMap<String, ArrayList<String>>();

// 创建ArrayList集合,并添加元素
ArrayList<String> sgyy = new ArrayList<String>();
sgyy.add("诸葛亮");
sgyy.add("赵云");

// 把ArrayList作为元素添加到HashMap集合
hm.put("三国演义", sgyy);

// 创建ArrayList集合,并添加元素
ArrayList<String> xyj = new ArrayList<String>();
sgyy.add("唐僧");
sgyy.add("孙悟空");

// 把ArrayList作为元素添加到HashMap集合
hm.put("西游记", xyj);

// 创建ArrayList集合,并添加元素
ArrayList<String> shz = new ArrayList<String>();
sgyy.add("武松");
sgyy.add("鲁智深");

// 把ArrayList作为元素添加到HashMap集合
hm.put("水浒传", shz);

// 遍历HashMap集合
Set<String> keySet = hm.keySet();
for (String key : keySet) {
    System.out.println(key);
    ArrayList<String> value = hm.get(key);
    for (String s : value) {
        System.out.println("\t" + s);
    }
}

案例:统计字符串中每个字符出现的次数

需求:键盘录入一个字符串,要求统计字符串中每个字符出现的次数。

Scanner sc = new Scanner(System.in);
System.out.println("请输入一个字符串:");
String line = sc.nextLine();

// HashMap<Character, Integer> hm = new HashMap<Character, Integer>(); // 无序
TreeMap<Character, Integer> hm = new TreeMap<Character, Integer>(); // 自然顺序
for (int i = 0; i < line.length(); i++) {
    char key = line.charAt(i);
    Integer value = hm.get(key);

    if (value == null) {
        hm.put(key, 1);
    } else {
        value++;
        hm.put(key, value);
    }
}

StringBuilder sb = new StringBuilder();
Set<Character> keySet = hm.keySet();
for (Character key : keySet) {
    Integer value = hm.get(key);
    sb.append(key).append("(").append(value).append(")");
}

String result = sb.toString();
System.out.println(result);

Collections

Collections概述和使用

Collections类的概述

  • 是针对集合操作的工具类

Collections类的常用方法

方法名描述
public static <T extends Comparable<? super T>> void sort(List list)将指定的列表按升序排序
public static void reverse(List<?> list)反转指定列表中元素的顺序
public static void shuffle(List<?> list)使用默认的随机源随机排列指定的列表
// 创建集合对象
        List<Integer> list = new ArrayList<Integer>();

        // 添加元素
        list.add(30);
        list.add(20);
        list.add(50);
        list.add(10);
        list.add(40);

        Collections.sort(list);
        System.out.println(list); // [10, 20, 30, 40, 50]

        Collections.reverse(list);
        System.out.println(list); // [50, 40, 30, 20, 10]

        Collections.shuffle(list);
        System.out.println(list); // [30, 50, 40, 10, 20]

案例:Arraylist集合存储学生对象并排序

需求:ArrayList存储学生对象,使用Collections对ArrayList进行排序
要求:按照年龄从小到大进行排序,年龄相同时,按照姓名的字母顺序排序

ArrayList<Student> array = new ArrayList<Student>();

Student s1 = new Student("linqingxia", 30);
Student s2 = new Student("zhangmanyu", 35);
Student s3 = new Student("wangzuxian", 33);
Student s4 = new Student("liuyan", 33);

array.add(s1);
array.add(s2);
array.add(s3);
array.add(s4);

Collections.sort(array, new Comparator<Student>() {
    @Override
    public int compare(Student s1, Student s2) {
        int num = s1.getAge() - s2.getAge();
        int num2 = num == 0 ? s1.getName().compareTo(s2.getName()) : num;
        return num2;
    }
});

for (Student s : array) {
    System.out.println(s.getName() + "," + s.getAge());
}

案例:模拟斗地主

需求:通过程序实现斗地主过程中的洗牌,发牌和看牌。

public static void main(String[] args) {
    // 创建牌盒
    ArrayList<String> array = new ArrayList<String>();

    // 定义花色数组
    String[] colors = {"♦", "♣", "♥", "♠"};
    // 定义点数数组
    String[] numbers = {"2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"};
    for (String color : colors) {
        for (String number : numbers) {
            array.add(color + number);
        }
    }
    array.add("小王");
    array.add("大王");

    // 洗牌
    Collections.shuffle(array);

    // 发牌
    ArrayList<String> lqxArray = new ArrayList<String>();
    ArrayList<String> lyArray = new ArrayList<String>();
    ArrayList<String> fqyArray = new ArrayList<String>();
    ArrayList<String> dpArray = new ArrayList<String>();

    for (int i = 0; i < array.size(); i++) {
        String poker = array.get(i);

        if (i >= array.size() - 3) {
            dpArray.add(poker);
        } else if (i % 3 == 0) {
            lqxArray.add(poker);
        } else if (i % 3 == 1) {
            lyArray.add(poker);
        } else if (i % 3 == 2) {
            fqyArray.add(poker);
        }
    }

    // 看牌
    lookPoker("林青霞", lqxArray);
    lookPoker("柳岩", lyArray);
    lookPoker("风清扬", fqyArray);
    lookPoker("底牌", dpArray);
}

// 看牌的方法
public static void lookPoker(String name, ArrayList<String> array) {
    System.out.print(name + "的牌是:");
    for (String poker : array) {
        System.out.print(poker + " ");
    }
    System.out.println();
}

案例:模拟斗地主升级版

需求:通过程序实现斗地主过程中的洗牌,发牌和看牌。
要求:对牌进行排序。

public static void main(String[] args) {
    HashMap<Integer, String> hm = new HashMap<Integer, String>();

    ArrayList<Integer> array = new ArrayList<Integer>();

    String[] colors = {"♦", "♣", "♥", "♠"};
    String[] numbers = {"3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A", "2"};

    int index = 0;
    for (String number : numbers) {
        for (String color : colors) {
            hm.put(index, color + number);
            array.add(index);
            index++;
        }
    }

    hm.put(index, "小王");
    array.add(index);
    index++;

    hm.put(index, "大王");
    array.add(index);

    Collections.shuffle(array);

    TreeSet<Integer> lqxSet = new TreeSet<Integer>();
    TreeSet<Integer> lySet = new TreeSet<Integer>();
    TreeSet<Integer> fqySet = new TreeSet<Integer>();
    TreeSet<Integer> dpSet = new TreeSet<Integer>();
    for (int i = 0; i < array.size(); i++) {
        int x = array.get(i);

        if (i >= array.size() - 3) {
            dpSet.add(x);
        } else if (i % 3 == 0) {
            lqxSet.add(x);
        } else if (i % 3 == 1) {
            lySet.add(x);
        } else if (i % 3 == 2) {
            fqySet.add(x);
        }
    }

    lookPoker("林青霞", lqxSet, hm);
    lookPoker("柳岩", lySet, hm);
    lookPoker("风清扬", fqySet, hm);
    lookPoker("底牌", dpSet, hm);
}

public static void lookPoker(String name, TreeSet<Integer> ts, HashMap<Integer, String> hm) {
    System.out.print(name + "的牌是:");
    for (Integer key : ts) {
        String poker = hm.get(key);
        System.out.print(poker + " ");
    }
    System.out.println();
}

IO流

File

File类概述和构造方法

  • File:它是文件和目录路径名的抽象表示。
  • 文件和目录是可以通过File封装成对象的
  • 对于File而言,其封装的并不是一个真正存在的文件,仅仅是一个路径名而已,它可以是存在的,也可以是不存在的,将来是要通过具体的操作把这个路径的内容转换为具体存在的。

File类构造方法

方法名描述
File(String pathname)通过将给定的路径名字符串转换为抽象路径名来创建新的File实例
File(String parent, String child)从父路径名字符串和子路径名字符串创建新的File实例
File(File parent, String child)从父抽象路径名和子路径名字符串创建新的File实例
File f1 = new File("E:\\itcast\\java.txt");
System.out.prinln(f1); // E:\itcast\java.txt

File f2 = new File("E:\\itcast", "java.txt");
System.out.prinln(f2); // E:\itcast\java.txt

File f3 = new File("E:\\itcast");
File f4 = new File(f3, "java.txt");
System.out.prinln(f4); // E:\itcast\java.txt

File类创建功能

方法名描述
public boolean creatNewFile()当具有该名称的文件不存在时,创建一个由该抽象路径名命名的空文件
public boolean mkdir()创建由此抽象路径名命名的目录
public boolean mkdirs()创建由此抽象路径名命名的目录,包括任何必需但不存在的父目录
File f1 = new File("E:\\itcast\\java.txt");
System.out.println(f1.creatNewFile()); // 文件不存在,就创建文件,返回true;文件存在,就不创建文件,返回false

File f2 = new File("E:\\itcast\\JavaSE");
System.out.println(f2.mkdir()); // 目录不存在,就创建目录,返回true;目录存在,就不创建目录,返回false

File f3 = new File("E:\\itcast\\JavaWEB\\HTML");
System.out.println(f3.mkdirs()); // 创建多级目录

File f4 = new File("E:\\itcast\\javase.txt");
System.out.println(f4.creatNewFile());

File类判断和获取功能

方法名描述
public boolean isDirectory()测试此抽象路径名表示的File是否为目录
public boolean isFile()测试此抽象路径名表示的File是否为文件
public boolean exists()测试此抽象路径名表示的File是否存在
public String getAbsolutePath()返回此抽象路径名的绝对路径名字符串
public String getPath()将此抽象路径名转换为路径名字符串
public String getName()返回由此抽象路径名表示的文件或目录的名称
public String[] list()返回此抽象路径名表示的目录中的文件和目录的名称字符串数组
public File[] listFiles()返回此抽象路径名表示的目录中的文件和目录的File对象数组

File类删除功能

方法名描述
public boolean delete()删除由此抽象路径名表示的文件或目录

绝对路径和相对路径的区别

  • 绝对路径:完整的路径名,不需要任何其他信息就可以定位它所表示的文件。例如:E:\itcast\java.txt
  • 相对路径:必须使用取自其他路径名的信息进行解释。例如:myFile\java.txt

删除目录时的注意事项

  • 如果一个目录中有内容(目录,文件),不能直接删除。应该先删除目录中的内容,最后才能删除目录

递归

递归概述:以编程的角度来看,递归指的是方法定义中调用方法本身的现象。

递归解决问题的思路

  • 把一个复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解
  • 递归策略只需少量的程序就可以描述出解题过程所需要的多次重复计算

递归解决问题要找到两个内容:

  • 递归出口:否则会出现内存溢出
  • 递归规则:与原问题相似的规模较小的问题

不死神兔

public static int f(int n) {
    if (n == 1 || n == 2) {
        return 1;
    } else {
        return f(n-1) + f(n - 2);
    }
}

System.out.println(f(20)); // 6765
案例:递归求阶乘

需求:用递归求5的阶乘,并把结果在控制台输出。

public static void main(String[] args) {
    int result = jc(5);
    System.out.println("5的阶乘是:" + result); // 120
}

public static int jc(int n) {
    if (n == 1) {
        return 1;
    } else {
        return n * jc(n - 1);
    }
}
案例:遍历目录

需求:给定一个路径(E:\itcast),请通过递归完成遍历该目录下的所有内容,并把所有文件的绝对路径输出在控制台。

public static void main(String[] args) {
    File srcFile = new File("E:\\itcast");

    getAllFilePath(srcFile);
}

public static void getAllFilePath(File srcFile) {
    File[] fileArray = srcFile.listFiles();
    if (fileArray != null) {
        for (File file : fileArray) {
            if (file.isDirectory()) {
                getAllFilePath(file);
            } else {
                System.out.println(file.getAbsolutePath());
            }
        }
    }
}

字节流

IO流概述和分类

IO流概述

  • IO:输入/输出(Input/Output)
  • 流:是一种抽象的概念,是对数据传输的总称。也就是说数据在设备间的传输成为流,流的本质是数据传输。
  • IO流就是用来处理设备间数据传输问题的
    • 常见的应用:文件复制、文件上传、文件下载

IO流分类

  • 按照数据的流向
    • 输入流:读数据
    • 输出流:写数据
  • 按照数据类型来分
    • 字节流
      • 字节输入流,字节输出流
    • 字符流
      • 字符输入流,字符输出流

一般来说,我们说IO流的分类是按照数据类型来分的

那么两种流都在什么情况下使用呢?

  • 如果数据通过Windows自带的记事本软件打开,我们还可以读懂里面的内容,就使用字符流,否则使用字节流。如果不知道该使用哪种类型的流,就使用字节流。

字节流写数据

字节流抽象基类

  • InputStream:这个抽象类是表示字节输入流的所有类的超类
  • OutputStream:这个抽象类是表示字节输出流的所有类的超类
  • 子类名特点:子类名称都是以其父类名作为子类名的后缀

FileOutputStream:文件输出流用于将数据写入File

  • FileOutputStream(String name):创建文件输出流以指定的名称写入文件

使用字节输出流写数据的步骤

  • 创建字节输出流对象(调用系统功能创建了文件,创建字节输出流对象,让字节输出流对象指向文件)
  • 调用字节输出流对象的写数据方法
  • 释放资源(关闭此文件输出流并释放与此流相关联的任何系统资源)
// 创建字节输出流对象
FileOutputStream fos = new FileOutputStream("myByteStream\\fos.txt");
// 将指定的字节写入文件输出流
fos.write(97);
// 释放资源
fos.close();

字节流写数据的3种方式

方法名描述
void write(int b)将指定的字节写入此文件输出流,一次写一个字节数据
void write(byte[] b)将b.length字节从指定的字节数组写入此文件输出流,一次写一个字节数组数据
void write(byte[] b, int off, int len)将len字节从指定的字节数组开始,从偏移量off开始写入此文件输出流,一次写一个字节数组的部分数据

字节流写数据的两个小问题

字节流写数据如何实现换行?

  • 写完数据后,加换行符
    • Windows:\r\n
    • Linux:\n
    • mac:\r

字节流写数据如何实现追加写入呢?

  • public FileOutputStream(String name, boolean append)
  • 创建文件输出流以指定的名称写入文件。如果第二个参数为true,则字节将写入文件的末尾而不是开头

字节流写数据加异常处理

  • finally:在异常处理时提供finally块来执行所有清除操作。比如说IO流中的释放资源。
  • 特点:被finally控制的语句一定会执行,除非JVM退出
try {
    可能出现异常的代码;
} catch (异常类名 变量名) {
    异常的处理代码;
} finally {
    执行所有清除操作;
}
FileOutputStream fos = null;

try {
    fos = new FileOutputStream("myByteStream\\fos.txt");
    fos.write("hello".getBytes());
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if (fos != null) {
        try {
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

字节流读数据(一次读一个字节数据)

FileInputStream:从文件系统中的文件获取输入字节

  • FileInputStream(String name):通过打开与实际文件的连接来创建一个FileInputStream,该文件由文件系统中的路径名name命名。

需求:把文件fos.txt中的内容读取出来在控制台输出。

FileInputStream fis = new FileInputStream("maByteStream\\fos.txt");
int by = fis.read();
System.out.println((char) by);
fis.close();
FileInputStream fis = new FileInputStream("maByteStream\\fos.txt");
int by;
while ((by = fis.read()) != -1) {
    System.out.println((char) by);
}
fis.close();

案例:复制文本文件

需求:把“E:\itcast\窗里窗外.txt”复制到模块目录下的“窗里窗外.txt”

FileInputStream fis = new FileInputStream("E:\\itcast\\窗里窗外.txt");
FileOutputStream fos = new FileOutputStream("myByteStream\\窗里窗外.txt");

int by;
while ((by = fis.read()) != -1) {
    fos.write(by);
}

fos.close();
fis.close();

字节流读数据(一次读一个字节数组数据)

需求:把文件fos.txt中的内容读取出来在控制台输出。

FileInputStream fis = new FileInputStream("myByteStream\\fos.txt");

byte[] bys = new byte[1024]; // 一般给1024及其整数倍
int len;
while ((len = fis.read(bys)) != -1) {
    System.out.println(new String(bys, 0, len));
}

fis.close();

案例:复制图片

需求:把“E:\itcast\mn.jpg”复制到模块目录下的“mn.jpg”

FileInputStream fis = new FileInputStream("E:\\\\itcast\\\\mn.jpg");
FileOutputStream fos = new FileOutputStream("myByteStream\\mn.jpg");

byte[] bys = new byte[1024];
int len;
while ((len = fis.read(bys)) != -1) {
    fos.write(bys, 0, len);
}

fis.close();
fos.close();

字节缓冲流

字节缓冲流

  • BufferedOutputStream:该类实现缓冲输出流。通过设置这样的输出流,应用程序可以向底层输出流写入字节,而不必为写入的每个字节导致底层系统的调用。
  • BufferedInputStream:创建BufferedInputStream将创建一个内部缓冲区数组。当从流中读取或跳过字节时,内部缓冲区将根据需要从所包含的输入流中重新填充,一次很多字节。

构造方法

  • 字节缓冲输出流:BufferedOutputStream(OutputStream out)
  • 字节缓冲输入流:BufferedInputStream(InputStream in)

为什么构造方法需要的是字节流,而不是具体的文件或者路径呢?

  • 字节缓冲流仅仅提供缓冲区,而真正的读写数据还得依靠基本的字节流对象进行操作。

案例:复制视频

需求:把“E:\itcast\字节流复制图片.avi”复制到模块目录下的“字节流复制图片.avi”

public static void main(String[] args) throws IOException {
    long startTime = System.currentTimeMillis();

    method1(); // 共耗时64565毫秒
    method2(); // 共耗时107毫秒
    method3(); // 共耗时405毫秒
    method4(); // 共耗时60毫秒

    long endTime = System.currentTimeMillis();
    System.out.println("共耗时:" + (endTime - startTime) + "毫秒");
}

// 基本字节流一次读写一个字节
public static void method1() throws IOException {
    FileInputStream fis = new FileInputStream("E:\\itcast\\字节流复制图片.avi");
    FileOutputStream fos = new FileOutputStream("muByteStream\\字节流复制图片.avi");

    int by;
    while ((by = fis.read()) != -1) {
        fos.write(by);
    }

    fos.close();
    fis.close();
}

// 基本字节流一次读写一个字节数组
public static void method2() throws IOException {
    FileInputStream fis = new FileInputStream("E:\\itcast\\字节流复制图片.avi");
    FileOutputStream fos = new FileOutputStream("muByteStream\\字节流复制图片.avi");

    byte[] bys = new byte[1024];
    int len;
    while ((len = fis.read(bys)) != -1) {
        fos.write(bys, 0, len);
    }

    fos.close();
    fis.close();
}

// 字节缓冲流一次读写一个字节
public static void method3() throws IOException {
    BufferedInputStream bis = new BufferedInputStream(new FileInputStream("E:\\itcast\\字节流复制图片.avi"));
    BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("muByteStream\\字节流复制图片.avi"));

    int by;
    while ((by = bis.read()) != -1) {
        bos.write(by);
    }

    bos.close();
    bis.close();
}

// 字节缓冲流一次读写一个字节数组
public static void method4() throws IOException {
    BufferedInputStream bis = new BufferedInputStream(new FileInputStream("E:\\itcast\\字节流复制图片.avi"));
    BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("muByteStream\\字节流复制图片.avi"));

    byte[] bys = new byte[1024];
    int len;
    while ((len = bis.read(bys)) != -1) {
        bos.write(bys, 0, len);
    }

    bos.close();
    bis.close();
}

字符流

为什么出现字符流

由于字节流操作中文不是特别方便,所以java就提供了字符流

  • 字符流 = 字节流 + 编码表

用字节流复制文本文件时,文本文件也会有中文,但是没有问题,原因是最终底层操作会自动将字节拼接成中文,如何识别是中文的呢?

  • 汉字在存储的时候,无论选择哪种编码存储,第一个字节都是负数

编码表

基础知识

  • 计算机中储存的信息都是用二进制数表示的,我们在屏幕上看到的英文、汉字等字符是二进制数转换之后的结果
  • 按照某种规则,将字符存储到计算机中,成为编码。反之,将在存储在计算机中的二进制数按照某种规则解析显示出来,称为解码。这里强调一下:按照A编码存储,必须按照A编码解析,这样才能显示正确的文本符号,否则就会导致乱码现象。
    • 字符编码:就是一套自然语言的字符与二进制数之间的对应规则(A —— 65)
HEX —— 十六进制
DEC —— 十进制
OCT —— 八进制
BIN —— 二进制

字符集

  • 是一个系统支持的所有字符的集合,包括各国家文字、标点符号、图形符号、数字等
  • 计算机要准确的存储和识别各种字符集符号,就需要进行字符编码,一套字符集必然至少有一套字符编码。常见的字符集有ASCII字符集、GBXXX字符集、Unicode字符集等

ASCII字符集

  • ASCII(American Standard Code for Information Interchange,美国信息交换标准代码):是基于拉丁字母的一套电脑编码系统,用于显示现代英语,主要包括控制字符(回车键、退格、换行键等)和可显示字符(英文大小写字符、阿拉伯数字和西文符号)
  • 基本的ASCII字符集,使用7位表示一个字符,共128字符。ASCII的扩展字符集使用8位表示一个字符,共258字符,方便支持欧洲常用字符。

GBXXX字符集

  • GB2312:简体中文码表。一个小于127的字符的意义与原来相同,但两个大于127的字符连在一起时,就表示一个汉字,这样大约可以组合了包含7000多个简体汉字,此外数学符号、罗马希腊的字母、日文的假名等都编进去了,连在ASCII里面本来就有的数字、标点、字母通通重新编了两个字节长的编码,这就是常说的“全角”字符,而原来在127号以下的那些就叫“半角”字符了
  • GBK:最常用的中文码表。实在GB2312标准基础上的扩展规范,使用了双字节编码方案,共收录了21003个汉字,完全兼容GB2312标准,同时支持繁体汉字以及日韩汉字等
  • GB18030:最新的中文码表。收录汉字70244个,采用多字节编码,每个字可以由1个、2个或4个字节组成。支持中国国内少数民族的文字,同时支持繁体汉字以及日韩汉字等

Unicode字符集

  • 为表达任意语言的任意字符而设计,是业界的一种标准,也称为统一码、标准万国码。它最多使用4个字节的数字来表达每个字母、符号,或者文字。有三种编码方案,UTF-8、UTF-16和UTF32.最为常用的是UTF-8编码
  • UTF-8编码:可以用来表示Unicode标准中任意字符,它是电子邮件、网页及其他存储或传送文字的应用中,优先采用的编码。互联网工作小组(IETF)要求所有互联网协议都必须支持UTF-8编码。它使用1-4个字节为每个字符编码
  • 编码规则:
    • 128个US-ASCII字符,只需1个字节编码
    • 拉丁文等字符,需要2个字节编码
    • 大部分常用字(含中文),使用3个字符编码
    • 其他极少使用的Unicode辅助字符,使用4个字节编码

小结:采用何种规则编码,就要采用对应规则解码,否则就会出现乱码

字符串中的编码解码问题

编码

  • byte[] getBytes():使用平台的默认字符集将该String编码为一系列字节,将结果存储到新的字节数组中
  • byte[] getBytes(String charsetName):使用指定的字符集将该String编码为一系列字节,将结果存储到新的字节数组中

解码

  • String(byte[] bytes):通过使用平台的默认字符集解码指定的字节数组来构造新的String
  • String(byte[] bytes, String charsetName):通过指定的字符集解码指定的字节数组来构造新的String

字符流中的编码解码问题

字符流抽象基类

  • Reader:字符输入流的抽象类
  • Writer:字符输出流的抽象类

字符流中和编码解码问题相关的两个类

  • InputStreamReader
  • OutputStreamWriter

字符流写数据的5中方式

方法名描述
void write(int c)写一个字符
void write(char[] cbuf)写入一个字符数组
void write(char[] cbuf, int off, int len)写入字符数组的一部分
void write(String str)写一个字符串
void write(String str, int off, int len)写一个字符串的一部分
方法名描述
flush()刷新流,还可以继续写数据
close()关闭流,释放资源,但是在关闭之前会先刷新流。一旦关闭,就不能再写数据

字符流读数据的2种方式

方法名描述
int read()一次读一个字符数据
int read(char[] cbuf)一次读一个字符数组数据

字符流复制Java文件

需求:把模块目录下的“ConversionStreamDemo.java”复制到模块目录下的“Copy.java”

InputStreamReader isr = new InputStreamReader(new FileInputStream("myByteStream\\ConversionStreamDemo.java"));
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("myByteStream\\Copy.java"));

// 一次读写一个字符数据
int ch;
while ((ch = isr.read()) != -1) {
    osw.write(ch);
}
osw.close();
isr.close();

// 一次读写一个字符数组
char[] chs = new char[1024];
int len;
while ((len = isr.read()) != -1) {
    osw.write(chs, 0, len);
}
osw.close();
isr.close();

案例:复制Java文件(改进版)

需求:把模块目录下的“ConversionStreamDemo.java”复制到模块目录下的“Copy.java”

FileReader fr = new FileReader("myByteStream\\ConversionStreamDemo.java");
FileWriter fw = new FileWriter("myByteStream\\Copy.java");

// 一次读写一个字符数据
int ch;
while ((ch = fr.read()) != -1) {
    fw.write(ch);
}
fw.close();
fr.close();

// 一次读写一个字符数组
char[] chs = new char[1024];
int len;
while ((len = fr.read()) != -1) {
    fw.write(chs, 0, len);
}
fw.close();
fr.close();

字符缓冲流

  • BufferWriter:将文本写入字符数输出流,缓冲字符,以提供单个字符,数组和字符串的高效写入,可以指定缓冲区大小,或者可以接受默认大小,默认值足够大,可用于大多数用途
  • BufferReader:从字符输入流读取文本,缓冲字符,以提供字符,数组和行的高效读取,可以指定缓冲区大小,或者可以使用默认大小,默认值足够大,可用于大多数用途

构造方法

  • BufferWriter(Writer out)
  • BufferReader(Reader in)

案例:复制Java文件(字符缓冲流改进版)

需求:把模块目录下的“ConversionStreamDemo.java”复制到模块目录下的“Copy.java”

BufferedReader br = new BufferedReader(new FileReader("myByteStream\\ConversionStreamDemo.java"));
BufferedWriter bw = new BufferedWriter(new FileWriter("myByteStream\\Copy.java"));

// 一次读写一个字符数据
int ch;
while ((ch = br.read()) != -1) {
    bw.write(ch);
}
bw.close();
br.close();

// 一次读写一个字符数组
char[] chs = new char[1024];
int len;
while ((len = br.read()) != -1) {
    bw.write(chs, 0, len);
}
bw.close();
br.close();

字符缓冲流特有功能

  • BufferReader
    • void newLine():写一行行分隔符,行分隔符字符串由系统属性定义
  • BufferWriter
    • public String readLine():读一行文字。结果包含行的内容的字符串,不包括任何终止字符,如果流的结尾已经到达,则为null

案例:字符缓冲流复制Java文件

需求:把模块目录下的“ConversionStreamDemo.java”复制到模块目录下的“Copy.java”

BufferedReader br = new BufferedReader(new FileReader("myByteStream\\ConversionStreamDemo.java"));
BufferedWriter bw = new BufferedWriter(new FileWriter("myByteStream\\Copy.java"));

String line;
while ((line = br.readLine()) != null) {
    bw.write(line);
    bw.newLine();
    bw.flush();
}

bw.close();
br.close();

IO流小结

小结:字节流可以复制任意文件数据,有4种方式,一般采用字节缓冲流一次读写一个字节数组的方式

小结:字符流只能复制文本数据,有5种方式,一般采用字符缓冲流的特有方式

案例:集合到文件

需求:把ArrayList集合中的字符串数据写入到文本文件。
要求:每一个字符串元素做为文本中的一行数据。

ArrayList<String> array = new ArrayList<String>();

array.add("hello");
array.add("world");
array.add("java");

BufferedWriter bw = new BufferedWriter(new FileWriter("myByteStream\\array.txr"));

for (String s : array) {
    bw.write(s);
    bw.newLine();
    bw.flush();
}

bw.close();

案例:文件到集合

需求:把文本文件的数据读取到集合中,并遍历集合。
要求:文件中每一行数据就是一个集合元素。

BufferedReader br = new BufferedReader(new FileReader("myByteStream\\array.txt"));

ArrayList<String> array = new ArrayList<String>();

String line;
while ((line = br.readLine()) != null) {
    array.add(line);
}

br.close();

for (String s : array) {
    System.out.println(s);
}

案例:点名器

需求:我有一个文件里面存满了班级同学的姓名,每一个姓名占一行,要求通过程序实现随机点名器。

BufferedReader br = new BufferedReader(new FileReader("myCharStream\\names.txt"));

ArrayList<String> array = new ArrayList<String>();
String line;
while ((line = br.readLine()) != null) {
    array.add(line);
}

br.close();

Random r = new Random();
int index = r.nextInt(array.size());

String name = array.get(index);

System.out.println("幸运者是:" + name);

案例:集合到文件(改进版)

需求:把ArrayList集合中的学生数据写入到文本文件。
要求:每一个学生对象的数据作为文件中的一行数据。

ArrayList<Student> array = new ArrayList<Student>();

Student s1 = new Student("itheima001", "林青霞", 30, "西安");
Student s2 = new Student("itheima002", "张曼玉", 35, "武汉");
Student s3 = new Student("itheima003", "王祖贤", 33, "郑州");

array.add(s1);
array.add(s2);
array.add(s3);

BufferedWriter bw = new BufferedWriter(new FileWriter("myCharStreanm\\student"));

for (Student s : array) {
    StringBuilder sb = new StringBuilder();
    sb.append(s.getSid).append(",").append(s.getName()).append(",").apppend(s.getAge()).append(",").append(s.getAddress);

    bw.write(sb.toString());
    bw.newLine();
    bw.flush();
}

bw.close();

案例:文件到集合(改进版)

需求:把文本文件中的数据读取到集合中,并遍历集合。
要求:文件中每一行数据就是一个学生对象的成员变量值。

BufferedReader br = new BufferedReader(new FileReader("myCharStream\\students.txt"));
ArrayList<Student> array = new ArrayList<Student>();

String line;
while ((line = br.readLine()) != null) {
    String[] strArray = line.split(",");
    Student s = new Student();

    s.setSid(strArray[0]);
    s.setName(strArray[1]);
    s.setAge(Integer.parseInt(strArray[2]));
    s.setAddress(strArray[3]);

    array.add(s);
}

br.close();

for (Student s : array) {
    System.out.println(s.getSid() + "." + s.getName() + "." + s.getAge() + "," + s.getAddress());
}

案例:集合到文件(数据排序改进版)

需求:键盘录入5个学生信息(姓名、语文成绩、数学成绩、英语成绩)。
要求:按照成绩总分从高到低写入文本文件。

TreeSet<Student> ts = new TreeSet<Student>(new Comparator<Student>() {
    @Override
    public int compare(Student s1, Student s2) {
        int num = s2.getSum() - s1.getSum();
        int num2 = num == 0 ? s1.getChinese() - s2.getChinese() : num;
        int num3 = num2 == 0 ? s1.getMath() - s2.getMath() : num2;
        int num4 = num3 == 0 ? s1.getName().compareTo(s2.getName()) : num3;
        return num4;
    }
});

for (int i = 0; i < 5; i++) {
    Scanner sc = new Scanner(System.in);
    System.out.println("请输入第" + (i + 1) + "个学生的信息:");
    System.out.println("姓名:");
    String name = sc.nextLine();
    System.out.println("语文成绩:");
    int chinese = sc.nextInt();
    System.out.println("数学成绩:");
    int math = sc.nextInt();
    System.out.println("英语成绩:");
    int english = sc.nextInt();

    Student s = new Student();
    s.setName(name);
    s.setChinese(chinese);
    s.setMath(math);
    s.setEngkish(english);

    ts.add(s);
}

BufferedWriter bw = new BufferedWriter(new FileWriter("myCharStream\\ts.txt"));

for (Student s : ts) {
    StringBuilder sb = new StringBuilder();
    sb.append(s.getName()).append(",").append(s.getChinese()).append(",").append(s.getMath()).append(",").append(s.getEnglish()).append(",").append(s.getSum());

    bw.write(sb.toString());
    bw.newLine();
    bw.flush();
}

bw.close();

案例:复制单级文件夹

需求:把“E:\itcast”这个文件夹复制到模块目录下。

public static void main(String[] args) throws IOException {
    File srcFolder = new File("E:\\itcast");

    String srcFolderName = srcFolder.getName();

    File destFolder = new File("myCharStream", srcFolderName);

    if (!destFolder.exists()) {
        destFolder.mkdir();
    }

    File[] listFiles = srcFolder.listFiles();

    for (File srcfile : listFiles) {
        String srcfileName = srcfile.getName();
        File destFile = new File(destFolder, srcfileName);
        copyFile(srcfile, destFile);
    }
}

public static void copyFile(File srcFile, File destFile) throws IOException {
    BufferedInputStream bis = new BufferedInputStream(new FileInputStream(srcFile));
    BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destFile));

    byte[] bys = new byte[1024];
    int len;
    while ((len = bis.read()) != -1) {
        bos.write(bys, 0, len);
    }

    bos.close();
    bis.close();
}

案例:复制多级文件夹

需求:把“E:\itcast”复制到F盘目录下。

public static void main(String[] args) throws IOException {
    File srcFile = new File("E:\\itcast");
    File destFile = new File("F:\\");

    copyFolder(srcFile, destFile);
}

private static void copyFolder(File srcFile, File destFile) throws IOException {
    if (srcFile.isDirectory()) {
        String srcFileName = srcFile.getName();
        File newFolder = new File(destFile, srcFileName);
        if (!newFolder.exists()) {
            newFolder.mkdir();
        }

        File[] listFiles = srcFile.listFiles();
        for (File file : listFiles) {
            copyFolder(file, newFolder);
        }
    } else {
        File newFile = new File(destFile, srcFile.getName());
        copyFile(srcFile, newFile);
    }
}

public static void copyFile(File srcFile, File destFile) throws IOException {
    BufferedInputStream bis = new BufferedInputStream(new FileInputStream(srcFile));
    BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destFile));

    byte[] bys = new byte[1024];
    int len;
    while ((len = bis.read()) != -1) {
        bos.write(bys, 0, len);
    }

    bos.close();
    bis.close();
}

复制文件的异常处理

try…catch…finally 的做法

try {
    可能出现异常的代码;
} catch (异常类名 变量名) {
    异常的处理代码;
} finally {
    执行所有清除操作;
}
public static void copyFile(File srcFile, File destFile) {
    BufferedInputStream bis = null;
    BufferedOutputStream bos = null;

    try {
        bis = new BufferedInputStream(new FileInputStream(srcFile));
        bos = new BufferedOutputStream(new FileOutputStream(destFile));

        byte[] bys = new byte[1024];
        int len;
        while ((len = bis.read()) != -1) {
            bos.write(bys, 0, len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (bos != null) {
            try {
                bos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        
        if (bis != null) {
            try {
                bis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

JDK7改进方案

try (定义流对象) {
    可能出现异常的代码;
} catch (异常类名 变量名) {
    异常的处理代码;
}

自动释放资源

public static void copyFile(File srcFile, File destFile) {
    try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(srcFile));
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destFile));) {
        byte[] bys = new byte[1024];
        int len;
        while ((len = bis.read()) != -1) {
            bos.write(bys, 0, len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

JDK9改进方案

定义输入流对象;
定义输出流对象;
try (输入流对象; 输出流对象) {
    可能出现异常的代码;
} catch (异常类名 变量名) {
    异常的处理代码;
}

自动释放资源

public static void copyFile(File srcFile, File destFile) throws IOException {
    BufferedInputStream bis = new BufferedInputStream(new FileInputStream(srcFile));
    BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destFile));
    try (bis; bos) {
        byte[] bys = new byte[1024];
        int len;
        while ((len = bis.read()) != -1) {
            bos.write(bys, 0, len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

特殊操作流

标准输入输出流

System类中有两个静态的成员变量:

  • public static final InputStream in:标准输入流。通常该流对应于键盘输入或由主机环境或用户指定的另一个输入源
  • public static final PrintStream out:标准输出流。通常该流对应于显示输出或由主机环境或用户指定的另一个输出目标
标准输入流

自己实现键盘录入数据

  • BufferedReader br = new BuffreredReader(new InputStreamReader(System.in));

写起来太麻烦了,java就提供了一个类实现键盘录入

  • Scanner sc = new Scanner(System.in);
标准输出流

输出语句的本质:是一个标准的输出流

  • PrintStream ps = System.out,
  • PrintStream类有的方法,System.out都可以使用

字节打印流

打印流分类

  • 字节打印流:PrintStream
  • 字符打印流:PrintWriter

打印流的特点

  • 只负责输出数据,不负责读取数据
  • 有自己的特有方法

字节打印流

  • PrintStream(String fileName):使用指定的文件名创建新的打印流
  • 使用继承父类的方法写数据,查看的时候会转码;使用自己的特有方法写数据,查看的数据原样输出
PrintString ps = new PrintString("myOtherStream\\ps.txt");

ps.print(97);
ps.println();
ps.print(98);

ps.println(97);
ps.println(98);

字符打印流

构造方法

方法名描述
PrintWriter(String fileName)使用指定的文件名创建一个新的PrintWriter,而不需要自动执行行刷新
PrintWriter(Writer out, boolean autoFlush)创建一个新的PrintWriter
out:字符输出流
autoFlush:一个布尔值,如果为真,则println,printf 或 format方法将刷新输出缓冲区
PrintWriter pw = new PrintWriter("myOtherStream\\pw.txt");

pw.write("hello");
pw.write("\r\n");
pw.flush();
pw.write("world");
pw.write("\r\n");
pw.flush();
pw.close();
PrintWriter pw = new PrintWriter(new FileWriter("myOtherStream\\pw.txt"), true);

pw.println("hello");
pw.println("world");
pw.close();

复制Java文件打印流改进版

需求:把模块目录下的“PrintStreamDemo.java”复制到模块目录下的“Copy.java”

BufferedReader br = new BufferedReader(new FileReader("myOtherStream\\PrintStreamDemo.java"));
PrintWriter pw = new PrintWriter(new FileWriter("myOtherStream\\Copy.java"), true);

String line;
while ((line = br.readLine()) != null) {
    pw.println(line);
}

pw.close();
br.close();

对象序列化流

  • 对象序列化:就是将对象保存到磁盘中,或者在网络中传输对象
  • 这种机制就是使用一个字节序列表示一个对象,该字节序列包含:对象的类型、对象的数据和对象中存储的属性等信息
  • 字节序列写到文件之后,相当于文件中持久保存了一个对象的信息
  • 反之,该字节序列还可以从文件中读取回来,重构对象,对它进行反序列化

要实现序列化和反序列化就要使用对象序列化流和对象反序列化流

  • 对象序列化流:ObjectOutputStream
  • 对象反序列化流:ObjectInputStream
对象序列化流

对象序列化流:ObjectOutputStream

  • 将Java对象的原始数据类型和图形写入OutputStream。可以使用ObjectInputStream读取(重构)对象。可以通过使用流的文件来实现对象的持久存储。如果流是网格套接字流,则可以在另一个主机上或另一个进程中重构对象。

构造方法

  • ObjectOutputStream(OutputStream out):创建一个写入指定的ObjectStream的ObjectOutputStream

序列化对象的方法

  • void writeString(Object obj):将制定的对象写入ObjectOutputStream

注意

  • 一个对象要想被序列化,该对象所属的类必须实现Serializable接口
  • Serializable是一个标记接口,实现该接口,不需要重写任何方法
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("myOthersStream\\oos.txt"));

Student s = new Student("林青霞", 30);

oos.writeObject(s);

oos.close();
对象反序列化流

对象反序列化流:ObjectInputStream

  • ObjectInputStream反序列化先前使用ObjectOutputStream编写的原始数据和对象

构造方法

  • ObjectInputStream(InputStream in):创建从指定的InputStream读取的ObjectInputStream

反序列化对象的方法

  • Object readObject():从ObjectInputStream读取一个对象
ObjectOutputStream ois = new ObjectOutputStream(new FileInputStream("myOthersStream\\oos.txt"));

Object obj = new ois.readObject();

Student s = (Student) obj;
System.out.println(s.getName() + "," + s.getAge());

ois.close();

对象序列化流问题

用对象序列化流序列化了一个对象后,假如修改了对象所属的类文件,读取数据会不会出问题?

  • 会出问题,抛出InvalidClassException异常

如果出问题了,如何解决?

  • 给对象所属的类加一个serialVersionUID
    • private static final serialVersionUID = 421;

如果一个对象中的某个成员变量的值不想被序列化,又该如何实现?

  • 给该成员变量加一个transient关键字,该关键字标记的成员变量不参与序列化过程

Properties

Properties概述

  • 是一个Map体系的集合类
  • Properties可以保存到流中或从流中加载

练习:Properties作为Map集合的使用

Properties prop = new Properties();

// 存储元素
prop.put("itheima001", "林青霞");
prop.put("itheima002", "张曼玉");
prop.put("itheima003", "王祖贤");

// 遍历集合
Set<Object> keySet = prop.keySet();
for (Object key : keySet) {
    Object value = prop.get(key);
    System.out.println(key + "," + value);
}

Properties作为集合的特有方法

方法名描述
Object setProperty(String key, String value)设置集合的键和值,都是String类型,底层调用HashTable方法 put
String getProperty(String key)使用此属性列表中指定的键搜索属性
Set stringPropertyName()从该属性列表中返回一个不可修改的键集,其中键及其对应的值是字符串
Properties prop = new Properties();

// 存储元素
prop.setProperty("itheima001", "林青霞");
prop.setProperty("itheima002", "张曼玉");
prop.setProperty("itheima003", "王祖贤");

System.out.println(prop.getProperty("itheima001"));

Set<String> names = prop.stringPropertyNames();
for (String key : names) {
    String value = prop.getProperty(key);
    System.out.println(key + "," + value);
}

Properties和IO流结合的方法

方法名描述
void load(Stream inStream)从输入字节流读取数组列表(键和元素对)
void load(Reader reader)从输入字符流读取属性列表(键和元素对)
void store(OutputStream out, String comments)将此属性列表(键和元素对)写入此Properties表中,以适合于使用load(InputStream)方法的格式写入输出字节流
void store(Writer writer, String comments)将此属性列表(键和元素对)写入此Properties表中,以适合使用load(Reader)方法的格式写入输出字符流

案例:游戏次数

需求:请写程序实现猜数字小游戏只能试玩3次,如果还想玩,提示:游戏试玩已结束,想玩请充值。

Properties prop = new Properties();

FileReader fr = new FileReader("myOtherStream\\game.txt");
prop.load(fr);
fr.close();

String count = prop.getProperty("count");
int number = Integer.parseInt(count);

if (number >= 3) {
    System.out.println("游戏试玩已结束,想玩请充值");
} else {
    GuessNumber.start();

    number++;
    prop.setProperty("count", String.valueOf(number));
    FileWriter fw = new FileWriter("myOtherStream\\game.txt");
    prop.store(fw, null);
    fw.close();
}

多线程

实现多线程

进程

进程:是正在运行的程序

  • 是系统进行资源分配和调用的独立单位
  • 每一个进程都有它自己的内存空间和系统资源

线程

线程:是进程中的单个顺序控制流,是一条执行路径

  • 单线程:一个进程如果只有一条执行路径,则称为单线程程序
  • 多线程:一个进程如果有多条执行路径,则成为多线程程序

多线程的实现方式1

继承Thread类

  • 定义一个MyThread类继承Thread类
  • 在MyThread类中重写run()方法
  • 创建MyThread类的对象
  • 启动线程

两个小问题

  • 为什么要重写run()方法?
    • 因为run()是用来封装被线程执行的代码
  • run()方法和start()方法的区别?
    • run():封装线程执行的代码,直接调用,相当于普通方法的调用
    • start():启动线程,然后由JVM调用此线程的run()方法
public class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(i);
        }
    }
}
public class MyThreadDemo {
    public static void main(String[] args) {
        MyThread my1 = new MyThread();
        MyThread my2 = new MyThread();

        // 启动线程
        my1.start();
        my2.start();
    }
}

设置和获取线程名称

Thread类中设置和获取线程名称的方法

  • void setName(String name):将此线程的名称更改为等于参数name
  • String getName():返回此线程的名称

线程调度

线程有两种调度模型

  • 分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片
  • 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,name会随机选择一个,优先级高的线程获取的CPU时间片相对多一些

Java使用的是抢占式调度模型。

  • 加入计算机只有一个CPU,那么CPU在某一时刻只能执行一条指令,线程只有得到CPU的时间片,也就是使用权,才可以执行指令。所以说多线程程序的执行有随机性,因为谁抢到CPU的使用权是不一定的。

Thread类中设置和获取线程优先级的方法

  • public final int getPriority():返回此线程的优先级
  • public final void setPriority(int newPriority):更改此线程的优先级
    • 线程默认优先级是5,线程优先级的范围是1-10
    • 线程优先级高仅仅表示获取CPU时间片的几率高,但是要在次数比较多或者多次运行的时候才能看到你想要的结果

线程控制

方法名说明
static void sleep(long millis)使当前正在执行的线程停留(暂停执行)指定的毫秒数
void join()等待这个线程死亡
void setDaemon(boolean on)将此线程标记为守护线程,当运行的线程都是守护线程时,Java虚拟机将退出

线程生命周期

多线程的实现方式2

实现Runnable接口

  • 定义一个MyRunnable实现Runnable接口
  • 在MyRunnable类中重写run()方法
  • 创建MyRunnable类的对象
  • 创建Thread类的对象,把MyRunnable对象作为构造方法的参数
    • Thread(Runnable target, String name)
  • 启动线程

多线程的实现方案有2中

  • 继承Thread类
  • 实现Runnable接口

相比继承Thread类,实现Runnable接口的好处

  • 避免了Java单继承的局限性
  • 适合多个相同程序的代码去处理同一个资源的情况,把线程和程序的代码、数据有效分离,较好地体现了面向对象的设计思想
MyRunnable my = new MyRunnable();

Thread t1 = new Thread(my, "高铁");
Thread t2 = new Thread(my, "飞机");

// 启动线程
t1.start();
t2.start();

线程同步

案例:卖票

需求:某电影院正在上映国产大片,共有100张票,而它有3个窗口售卖,请设计一个程序实现模拟该电影院卖票。

public class SellTicket implements Runnable {
    private int tickets = 100;

    @Override
    public void run() {
        while (true) {
            if (tickets > 0) {
                System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
                tickets--;
            }
        }
    }
}
public class SellTicketDemo {
    public static void main(String[] args) {
        SellTicket st = new SellTicket();

        Thread t1 = new Thread(st, "窗口一");
        Thread t2 = new Thread(st, "窗口二");
        Thread t3 = new Thread(st, "窗口三");

        // 启动线程
        t1.start();
        t2.start();
        t3.start();
    }
}

卖票案例的思考

要求:每次出票时间100毫秒,用sleep()方法实现。

public class SellTicket implements Runnable {
    private int tickets = 100;
    private Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
                if (tickets > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
                    tickets--;
                }
            }
        }
    }
}

为什么会出现安全问题?

  • 是否是多线程环境
  • 是否有数据共享
  • 是否有多条语句操作共享数据

如何解决多线程安全问题?

  • 基本思想:让程序没有安全问题的环境

如何实现?

  • 把多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可
  • Java提供了同步代码块的方式来解决

同步代码块

锁多条语句操作共享数据,可以使用同步代码块实现

  • 格式
synchronized(任意对象) {
    多条语句操作共享数据的代码;
}
  • synchronized(任意对象):就相当于给代码加锁了,任意对象就可以看成是一把锁

同步的好处和弊端

  • 好处:解决了多线程的数据安全问题
  • 弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率

同步方法

  • 同步方法:把synchronized关键字加到方法上
  • 格式:
修饰符 synchronized 返回值类型 方法名(方法参数) {}

同步方法的锁对象是什么?

  • this

  • 同步静态方法:把synchronized关键字加到静态方法上
  • 格式:
修饰符 static synchronized 返回值类型 方法名(方法参数) {}

同步静态方法的锁对象是什么?

  • 类名.this

线程安全的类

StringBuffer
  • 线程安全,可变的字符序列
  • 从版本JDK5开始,被StringBuilder替代。通常应该使用StringBuilder类,因为它支持所有相同的操作,但它更快,因为它不执行同步
Vector
  • 从Java平台v1.2开始,该类改进了List接口,使其成为Java Collections Framework的成员。与新的集合实现不同,Vector被同步,如果不需要线程安全的实现,建议使用ArrayList代替Vector
HashTable
  • 该类实现了一个哈希表,它将键映射到值。任何非null对象都可以用作键或者值
  • 从Java平台v1.2开始,该类进行了改进,实现了Map接口,使其成为Java Collections Framework的成员。与新的集合实现不同,HashTable被同步。如果不需要线程侵权的实现,建议使用HashMap代替Hashtable

Lock锁

Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作

Lock中提供了获得锁和释放锁的方法

  • void lock():获得锁
  • void unlock():释放锁

Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化

ReentrantLock的构造方法

  • ReentrantLock():创建一个ReentrantLock的实例
public class SellTicket implements Runnable {
    private int tickets = 100;
    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            try {
                lock.lock();
                if (tickets > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
                    tickets--;
                }
            } finally {
                lock.unlock();
            }
        }
    }
}

生产者消费者

生产者消费者模式概述

生产者消费者模式是一个十分经典的多线程协作的模式,弄懂生产者消费者问题能够让我们对多线程编程的理解更加深刻。

所谓的生产者消费者问题,实际上主要是包含了两类线程:

  • 一类是生产者线程用余生产数据
  • 一类是消费者线程用于消费数据

为了解偶生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库

  • 生产者生产数据之后直接放置在公共数据区中,并不需要关心消费者的行为
  • 消费者只需要从共享数据区中去取数据,并不需要关心生产者的行为

Object类中的等待和唤醒方法:

方法名描述
void wait()导致当前线程等待,直到另一个线程调用该对象的notify()方法或notifyAll()方法
void notify()唤醒正在等待对象监视器的单个线程
void notifyAll()唤醒正在等待对象监视器的所有线程

生产者消费者案例

public class Box {
    private int milk;
    private boolean state = false;

    public synchronized void put(int milk) {
        // 如果有牛奶,等待消费
        if (state) {
            try {
                wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

        // 如果没有牛奶,就生产牛奶
        this.milk = milk;
        System.out.println("送奶工将第" + this.milk + "瓶奶放入奶箱");

        // 修改完毕之后,修改奶箱状态
        state = true;

        // 唤醒其他等待的线程
        notifyAll();
    }

    public synchronized void get() {
        // 如果没有牛奶,应该等待生产
        if (!state) {
            try {
                wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

        // 如果有牛奶,就消费牛奶
        System.out.println("用户拿到第" + this.milk + "瓶奶");

        // 消费完毕之后,修改奶箱状态
        state = false;

        // 唤醒其他等待的线程
        notifyAll();
    }
}
public class Producer implements Runnable {
    private Box b;

    public Producer(Box b) {
        this.b = b;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            b.put(i);
        }
    }
}
public class Customer implements Runnable {
    private Box b;

    public Customer(Box b) {
        this.b = b;
    }

    @Override
    public void run() {
        while (true) {
            b.get();
        }
    }
}
public class BoxDemo {
    public static void main(String[] args) {
        Box b= new Box();

        Producer p =new Producer(b);

        Customer c = new Customer(b);

        Thread t1 = new Thread(p);
        Thread t2 = new Thread(c);

        t1.start();
        t2.start();
    }
}

网络编程

网络编程入门

网络编程概述

计算机网络

  • 是指将地理位置不同的具有独立功能的多台计算机及其外部设备,通过通信线路连接起来,在网络操作系统,网络管理软件及网络通信协议的管理和协调下,实现资源共享和信息传递的计算机系统

网络编程

  • 在网络通信协议下,实现网络互联的不同计算机上运行的程序间可以进行数据交换

网络编程三要素

  • IP地址
    • 要想让网络中的计算机能够互相通信,必须为每台计算机指定一个标识号,通过这个标识号来指定要接收数据的计算机和识别发送的计算机,而IP地址就是这个标识号。也就是设备的标识
  • 端口
    • 网络的通信,本质上是两个应用程序的通信。每台计算机都有很多的应用程序,那么在网络通信时,如何区分这些应用程序呢?如果说IP地址可以唯一标识网络中的设备,那么端口号就可以唯一标识设备中的应用程序了。也就是应用程序的标识
  • 协议
    • 通过计算机网络,可以使多台计算机实现连接,位于同一个网络中的计算机在进行连接和通信时需要遵守一定的规则。在计算机网络中,这些连接和通信的规则被称为网络通信协议,它对数据的传输格式、传输速率、传输步骤等做了统一的规定,通信双方必须同时遵守才能完成数据交换。常见的协议有UDP协议和TCP协议

IP地址

IP地址:是网络中设备的唯一标识

IP地址分为两大类

  • IPV4:是给每个连接在网络上的主机分配一个32bit地址。按照TCP/IP规定,IP地址用二进制来表示,每个IP地址长32bit,也就是4个字节。例如一个采用二进制形式的IP地址是“11000000 10101000 00000001 01000010”,这么长的地址,处理起来太费劲了。为了方便使用,IP地址经常被写成十进制的形式,中间使用符号“.”分隔不同的字节。于是,上面的IP地址可以表示为“192.168.1.66”。IP地址的这种表示法叫做“点分十进制表示法”,这显然比1和0容易记忆很多
  • IPV6:由于互联网的蓬勃发展,Ip地址的需求量愈来愈大,但是网络地址资源有限,使得IP的分配越发紧张。为了扩大地址空间,通过IPV6重新定义地址空间,采用128位地址长度,每16个字节一组,分成8组十六进制数,这样就解决了网络地址资源数量不够的问题

常用命令

  • ipconfig:查看本机IP地址
  • ping IP地址:检查网络是否连通

特殊IP地址

  • 127.0.0.1:是回送地址,可以代表本机地址,一般用来测试使用

InetAddress的使用

InetAddress:此类表示Internet协议(IP)地址

方法名描述
static InetAddress getByName(String host)确定主机名称的IP地址。主机名称可以使机器名称,也可以是IP地址
String getHostName()获取此IP地址的主机名
String getHostAddress()返回文本显示中的IP地址字符串
// InetAddress address = InetAddress.getByName("DESKTOP-VJICL7M");
InetAddress address = InetAddress.getByName("192.168.1.5");

String name = address.getHostName();
String ip = address.getHostAddress();

System.out.println("主机名" + name);
System.out.println("IP地址" + ip);

端口

端口:设备上应用程序的唯一标识

端口号:用两个字节表示的整数,它的取值范围是065535.其中,01023之间的端口号用于一些知名的网络服务和应用,普通的应用程序需要使用1024以上的端口号。如果端口号被另一个服务或应用所占用,会导致当前程序启动失败。

协议

协议:计算机网络中,连接和通信的规则被称为网络通信协议。

UDP协议

  • 用户数据服务协议(User Datagram Protcol)
  • UDP是无连接通信协议。即在传输数据时,数据的发送端和接收端不建立逻辑连接。简单来说,当一台计算机向另一台计算机发送数据时,发送端不会确认接收端是否存在,就会发出数据,同样接收端在收到数据时,也不会向发送端反馈是否收到数据。
  • 由于UDP协议消耗资源小,通信效率高,所以通常都会用于音频、视频和普通数据的传输
  • 例如视频会议通常采用UDP协议,因为这种情况即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。但是使用UDP协议传送数据时,由于UDP的面向无连接性,不能保证数据的完整性,因此在传输重要数据时不建议使用UDP协议

TCP协议

  • 传输控制协议(Transmission Control Protocol)
  • TCP协议是面向连接的通信协议,即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。在TCP连接中必须要明确客户端与服务器端,由客户端向服务端发出连接请求,每次连接的创建都需要经过“三次握手”
  • “三次握手”:TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠
    • 第一次握手,客户端向服务器端发出连接请求,等待服务器确认
    • 第二次握手,服务器端向客户端会送一个响应,通知客户端收到了连接请求
    • 第三次握手,客户端再次向服务器端发送确认信息,确认连接
  • 完成三次握手,连接建立后,客户端和服务器就可以开始进行数据传输了。由于这种面向连接的特性,TCP洗衣可以保证传输数据的安全,所以应用十分广泛。例如上传文件、下载文件、浏览网页等

UDP通信程序

UDP发送数据

发送数据的步骤

  1. 创建发送端的Socket对象(DatagramSocket)
  2. 创建数据,并把数据打包
  3. 调用DatagramSocket对象的方法发送数据
  4. 关闭发送端
DatagramSocket ds = new DatagramSocket();

byte[] bys = "hello,udp,我来了".getBytes();
/*int length = bys.length;
InetAddress address = InetAddress.getByName("192.168.1.66");
int port = 10086;
DatagramPacket dp = new DatagramPacket(bys, length, address, port);*/
DatagramPacket dp = new DatagramPacket(bys, bys.length, InetAddress.getByName("192.168.1.66"), 10086);

ds.send(dp);

ds.close();

UDP接收数据

接收数据的步骤

  1. 创建接收端的Socket对象(DatagramSocket)
  2. 创建一个数据包,用于接收数据
  3. 调用DatagramSocket对象的方法接收数据
  4. 解析数据包,并把数据在控制台显示
  5. 关闭接收端
DatagramSocket ds = new DatagramSocket(10086);

byte[] bys = new byte[1024];
DatagramPacket dp = new DatagramPacket(bys, bys.length);

ds.receive(dp);

byte[] datas = dp.getData();
int len = dp.getLength();
String dataString = new String(datas, 0, len);
System.out.println("数据是:" + dataString);

ds.close();

UDP通信程序练习

按照下面的要求实现程序:

  • UDP发送数据:数据来自于键盘录入,直到输入的数据是886,发送数据结束
  • UDP接收数据:因为接收端不知道发送端什么时候停止发送,故采用死循环接收
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

public class SendDemo {
    public static void main(String[] args) throws IOException {
        DatagramSocket ds = new DatagramSocket();

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String line;
        while ((line = br.readLine()) != null) {
            if ("886".equals(line)) {
                break;
            }
            
            byte[] bys = line.getBytes();
            DatagramPacket dp = new DatagramPacket(bys, bys.length, InetAddress.getByName("192.168.1.66"), 12345);
            
            ds.send(dp);
        }
        
        ds.close();
    }
}
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;

public class ReceiveDemo {
    public static void main(String[] args) throws IOException {
        DatagramSocket ds = new DatagramSocket(12345);

        while (true) {
            byte[] bys = new byte[1024];
            DatagramPacket dp = new DatagramPacket(bys, bys.length);

            ds.receive(dp);

            System.out.println("数据是:" + new String(dp.getData(), 0, dp.getLength()));
        }
    }
}

TCP通信程序

TCP通信原理

TCP通信协议是一种可靠的网络协议,它在通信的两端各建立一个Socket对象,从而在通信的两端形成网络虚拟链路,一旦建立了虚拟的网络链路,两端的程序就可以通过虚拟链路进行通信。

Java对基于TCP协议的网络提供了良好的封装,使用Socket对象;来代表两端的通信端口,并通过Socket产生的IO流来进行网络通信。

Java为客户端提供了Socket类,为服务器端提供了ServerSocket类。

TCP发送数据

发送数据的步骤

  1. 创建客户端的Socket对象(Socket)
  2. 获取输出流,写数据
  3. 释放资源
// Socket s = new Socket(InetAddress.getByName("192.168.1.66"), 10086);
Socket s = new Socket("192.168.1.66", 10086);

OutputStream os = s.getOutputStream();

os.write("hello java,我来了".getBytes());

s.close();

TCP接收数据

接收数据的步骤

  1. 创建服务器端的Socket对象(ServerSocket)
  2. 获取输入流,读数据,并把数据显示在控制台
  3. 释放资源
ServerSocket ss = new ServerSocket(10086);

Socket s = ss.accept();
InputStream is = s.getInputStream();

byte[] bys = new byte[1024];
int len = is.read(bys);
String data = new String(bys, 0, len);
System.out.println("数据是:" + data);

s.close();
ss.close();

TCP通信程序练习1

  • 客户端:发送数据,接收服务器反馈
  • 服务端:接收数据,给出反馈
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class ClientDemo {
    public static void main(String[] args) throws IOException {
        Socket s = new Socket("192.168.1.66", 10086);

        OutputStream os = s.getOutputStream();
        os.write("hello java,我来了".getBytes());

        InputStream is = s.getInputStream();
        byte[] bys = new byte[1024];
        int len = is.read();
        String data = new String(bys, 0, len);
        System.out.println("客户端:" + data);

        s.close();
    }
}
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class ServerDemo {
    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(10086);

        Socket s = ss.accept();
        InputStream is = s.getInputStream();
        byte[] bys = new byte[1024];
        int len = is.read(bys);
        String data = new String(bys, 0, len);
        System.out.println("服务器:" + data);
        
        // 给出反馈
        OutputStream os = s.getOutputStream();
        os.write("数据已经收到".getBytes());

        ss.close();
    }
}

TCP通信程序练习2

  • 客户端:数据来自于键盘录入,直到输入的数据是886,发送数据结束
  • 服务器:接收到的数据在控制台输出
import java.io.*;
import java.net.Socket;

public class ClientDemo {
    public static void main(String[] args) throws IOException {
        Socket s = new Socket("192.168.1.66", 10086);

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        // 封装输出流对象
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
        
        String line;
        while ((line = br.readLine()) != null) {
            if ("886".equals(line)) {
                break;
            }
            
            // 获取输出流对象
            bw.write(line);
            bw.newLine();
            bw.flush();
        }
        
        s.close();
    }
}
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

public class ServerDemo {
    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(10086);

        // 监听客户端的连接,返回一个对应的Socket对象
        Socket s = ss.accept();

        // 获取输入流
        /*InputStream is = s.getInputStream();
        InputStreamReader isr = new InputStreamReader(is);
        BufferedReader br = new BufferedReader(isr);*/
        BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
        String line;
        while ((line = br.readLine()) != null) {
            System.out.println(line);
        }

        // 释放资源
        ss.close();
    }
}

TCP通信程序练习3

  • 客户端:数据来自于键盘录入,直到输入的数据是886,发送数据结束
  • 服务器:接收到的数据写入文本文件
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class ServerDemo {
    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(10086);

        // 监听客户端的连接,返回一个对应的Socket对象
        Socket s = ss.accept();

        // 获取输入流
        BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
        // 把数据写入文本文件
        BufferedWriter bw = new BufferedWriter(new FileWriter("myNet\\s.txt"));
        String line;
        while ((line = br.readLine()) != null) {
            bw.write(line);
            bw.newLine();
            bw.flush();
        }

        // 释放资源
        bw.close();
        ss.close();
    }
}

TCP通信程序练习4

  • 客户端:数据来自于文本文件
  • 服务器:接收到的数据写入文本文件
import java.io.*;
import java.net.Socket;

public class ClientDemo {
    public static void main(String[] args) throws IOException {
        Socket s = new Socket("192.168.1.66", 10086);

        BufferedReader br = new BufferedReader(new FileReader("myNet\\InetaddressDemo.txt"));
        // 封装输出流对象
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));

        String line;
        while ((line = br.readLine()) != null) {
            bw.write(line);
            bw.newLine();
            bw.flush();
        }

        // 释放资源
        br.close();
        s.close();
    }
}

TCP通信程序练习5

  • 客户端:数据来自于文本文件,接收服务器反馈
  • 服务器:接收到的数据写入文本文件,给出反馈

  • 出现问题:程序一直等待
  • 原因:读数据的方法是阻塞式的
  • 解决办法:自定义结束标记,或使用shutdownOuput()方法(推荐)
import java.io.*;
import java.net.Socket;

public class ClientDemo {
    public static void main(String[] args) throws IOException {
        Socket s = new Socket("192.168.1.66", 10086);

        BufferedReader br = new BufferedReader(new FileReader("myNet\\InetaddressDemo.txt"));
        // 封装输出流对象
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));

        String line;
        while ((line = br.readLine()) != null) {
            bw.write(line);
            bw.newLine();
            bw.flush();
        }
        
        // 自定义结束标记
        /*bw.write("886");
        bw.newLine();
        bw.flush();*/
        
        s.shutdownOutput();

        // 给出反馈
        BufferedWriter bwServer = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
        bwServer.write("文件上传成功");
        bwServer.newLine();
        bwServer.flush();

        // 释放资源
        br.close();
        s.close();
    }
}
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class ServerDemo {
    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(10086);

        // 监听客户端的连接,返回一个对应的Socket对象
        Socket s = ss.accept();

        // 获取输入流
        BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
        // 把数据写入文本文件
        BufferedWriter bw = new BufferedWriter(new FileWriter("myNet\\s.txt"));
        String line;
        while ((line = br.readLine()) != null) {
            /*if ("886".equals(line)) {
                break;
            }*/

            bw.write(line);
            bw.newLine();
            bw.flush();
        }

        // 接收反馈
        BufferedReader brClient = new BufferedReader(new InputStreamReader(s.getInputStream()));
        String data = brClient.readLine();
        System.out.println("服务器的反馈:" + data);

        // 释放资源
        bw.close();
        ss.close();
    }
}

TCP通信程序练习6

  • 客户端:数据来自于文本文件,接收服务器反馈
  • 服务器:接收到的数据写入文本文件,给出反馈,代码用线程进行封装,为每一个客户端开启一个线程
import java.io.*;
import java.net.Socket;

public class serverThread implements Runnable {
    private Socket s;

    public serverThread(Socket s) {
        this.s = s;
    }

    @Override
    public void run() {
        // 接收数据,写入文本文件
        try {
            BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));

            // 解决名称冲突问题
            int count = 0;
            File file = new File("myNet\\Copy[" + count + "].java");

            while (file.exists()) {
                count++;
                file = new File("myNet\\Copy[" + count + "].java");
            }

            BufferedWriter bw = new BufferedWriter(new FileWriter(file));

            String line;
            while ((line = br.readLine()) != null) {
                bw.write(line);
                bw.newLine();
                bw.flush();
            }

            // 给出反馈
            BufferedWriter bwServer = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
            bwServer.write("文件上传成功");
            bwServer.newLine();
            bw.flush();

            // 释放资源
            s.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class ServerDemo {
    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(10086);

        while (true) {
            // 监听客户端的连接,返回一个对应的Socket对象
            Socket s = ss.accept();

            // 为每一个客户端开启一个线程
            new Thread(new serverThread(s)).start();
        }
    }
}

Lambda表达式

体验Lambda表达式

需求:启动一个线程,在控制台输出一句话:多线程启动了

方式1

  1. 定义一个类MyRunnable实现Runnable接口,重写run()方法
  2. 创建MyRunnable类的对象
  3. 创建Thread类的对象,把MyRunnable类的对象作为构造参数传递
  4. 启动线程

方式2

  • 匿名内部类的方式改进

方式3

  • Lambda表达式的方式改进
public class LambdaDemo {
    public static void main(String[] args) {
        /*MyRunnable my = new MyRunnable();
        Thread t = new Thread(my);
        t.start();*/

        // 使用匿名内部类的方式改进
        // new Thread(new Runnable() {
        //     @Override
        //     public void run() {
        //         System.out.println("多线程徐启动了");
        //     }
        // }).start();

        // Lambda表达式改进
        new Thread( () -> {
            System.out.println("多线程徐启动了");
        } ).start();
    }
}

Lambda表达式的标准格式

匿名内部类中重写run()方法的代码分析

new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("多线程徐启动了");
    }
}).start();
  • 方法形式参数为空,说明调用方法时不需要传递参数
  • 方法返回值类型为void,说明方法执行没有结果返回
  • 方法体中的内容,是我们具体要做的事情

Lambda表达式的代码分析

new Thread( () -> {
    System.out.println("多线程徐启动了");
} ).start();
  • ():里面没有内容,可以看成是方法形式参数为空
  • ->:用箭头指向后面要做的事情
  • {}:包含一段代码,我们称之为代码块,可以看成是方法体中的内容

组成Lambda表达式的三要素:形式参数、箭头、代码块

Lambda表达式的格式

  • 格式:
(形式参数) -> {代码块}
  • 形式参数:如果有多个参数,参数之间用逗号隔开;如果没有参数,留空即可
  • ->:由英文中划线和大于符号组成,固定写法,代表指向动作
  • 代码块:是我们具体要做的事情,也就是以前我们写的方法体内容

Lambda表达式的练习

Lambda表达式的使用前提

  • 有一个接口
  • 接口中有且仅有一个抽象方法

练习1

  • 定义一个接口(Eatable),里面定义一个抽象方法:void eat()
  • 定义一个测试类(EatableDemo),在测试类中提供两个方法:
    • 一个方法是:useEatable(Eatable e)
    • 一个方法是主方法:在主方法中调用useEatable()方法
public interface Eatable {
    void eat();
}
public class EatableImpl implements Eatable {
    @Override
    public void eat() {
        System.out.println("一天一苹果,医生远离我");
    }
}
public class EatableDemo {
    public static void main(String[] args) {
        Eatable e = new EatableImpl();
        useEatable(e);

        // 匿名内部类
        useEatable(new EatableImpl() {
            @Override
            public void eat() {
                System.out.println("一天一苹果,医生远离我");
            }
        });

        // Lambda表达式
        useEatable(() -> {
            System.out.println("一天一苹果,医生远离我");
        });
    }

    private static void useEatable(Eatable e) {
        e.eat();
    }
}

Lambda表达式的使用

练习2

  • 定义一个接口(Flyable),里面定义一个抽象方法:void fly(String s)
  • 定义一个测试类(FlyableDemo),在测试类中提供两个方法:
    • 一个方法是:useFlyable(Flyable f)
    • 一个方法是主方法:在主方法中调用useFlyable()方法
public interface Flyable {
    void fly(String s);
}
public class FlyableDemo {
    public static void main(String[] args) {
        useFlyable((String s) -> {
            System.out.println(s);
            System.out.println("飞机自驾游");
        });
    }

    private static void useFlyable(Flyable f) {
        f.fly("风和日丽,晴空万里");
    }
}

Lambda表达式的使用

练习3

  • 定义一个接口(Addable),里面定义一个抽象方法:void add(int x, int y)
  • 定义一个测试类(AddableDemo),在测试类中提供两个方法:
    • 一个方法是:useAddable(Addable a)
    • 一个方法是主方法:在主方法中调用useAddable()方法
public interface Addable {
    int add(int x, int y);
}
public class AddableDemio {
    public static void main(String[] args) {
        useAddable((int x, int y) -> {
            return x + y;
        });
    }

    public static void useAddable(Addable a) {
        int sum = a.add(10, 20);
        System.out.println(sum);
    }
}

Lambda表达式的省略模式

省略规则

  • 参数类型可以省略,但有多个参数的情况下,不能只省略一个
  • 如果参数有且仅有一个,那么小括号可以省略
  • 如果代码块的语句只有一条,可以省略大括号和分号,如果有return,可以省略return

Lambda表达式的注意事项

注意事项

  • 使用Lambda必须要有接口,并且要求接口中有且仅有一个抽象方法
  • 必须要有上下文环境,才能推导出Lambda对应的接口
    • 根据局部变量的赋值得知Lambda对应的接口:Runnable r = () -> System.out.println("Lambda表达式");
    • 根据调用方法的参数得知Lambda对应的接口:new Thread() -> System.out.println("Lambda表达式").start();

Lambda表达式和匿名内部类的区别

  • 所需类型不同

    • 匿名内部类:可以是接口,也可以是抽象类,还可以是具体类
    • Lambda表达式:只能是接口
  • 使用限制不同

    • 如果接口中有且仅有一个抽象方法,可以使用呢名内部类,也可以使用Lambda表达式
    • 如果接口中有多于一个抽象方法,只能使用匿名内部类,而不能使用Lambda表达式
  • 实现原理不同

    • 匿名内部类:编译之后,产生一个单独的class字节码文件
    • Lambda表达式:编译之后,没有一个单独的class字节码文件。对应的字节码会在运行的时候动态生成

接口组成更新

接口组成更新描述

接口的组成

  • 常量
    • public static final
  • 抽象方法
    • public abstract
  • 默认方法(Java8)
  • 静态方法(Java8)
  • 私有方法(Java9)

接口中默认方法

默认方法的定义格式

  • 格式:
public default 返回值类型 方法名(参数列表) {}

接口中默认方法注意事项

  • 默认方法不是抽象方法,所以不强制被重写,但是可以被重写,重写的时候去掉default关键字
  • public可以省略,default不能省略

接口中静态方法

接口中静态方法定义格式

  • 格式:
public static 返回值类型 方法名(参数列表) {} 

接口中静态方法的注意事项

  • 静态方法只能通过接口名调用,不能通过实现类名或者对象名调用
  • public 可以省略,static不能省略

接口中私有方法

接口中私有方法定义格式

  • 格式1:
private 返回值类型 方法名(参数列表) {}
  • 格式2:
private static 返回值类型 方法名(参数列表) {}

接口中私有方法注意事项

  • 默认方法可以调用私有的静态方法和非静态方法
  • 静态方法只能调用私有的静态方法

方法引用

体验方法引用

public interface Printable {
    void printString(String s);
}
public class PrintableDemo {
    public static void main(String[] args) {
        usePrintable(s -> System.out.println(s));

        // 方法引用符:::
        usePrintable(System.out::println);
    }

    public static void usePrintable(Printable p) {
        p.printString("爱生活爱Java");
    }
}

方法引用符

  • :: 该符号为引用运算符,而它所在的表达式被称为方法引用

推导与省略

  • 如果使用Lambda,那么根据“可推导就是可省略”的原则,无需指定参数类型,也无需指定重载形式,它们都将被自动推导
  • 如果使用方法引用,也是同样可以根据上下文进行推导
  • 方法引用是Lambda的孪生兄弟

Lambda表达式支持的方法引用

常见的引用方式

  • 引用类方法
  • 引用对象的实例方法
  • 引用类的实例方法
  • 引用构造器

引用类方法

  • 引用类方法,其实就是引用类的静态方法
  • 格式:
类名::静态方法
  • 范例:
Integer::parseInt
  • Integer类的方法:public static parseInt(String s) 将此String转换为int类型数据

引用对象的实例方法

  • 引用对象的实例方法,其实就是引用类中的成员方法
  • 格式:
对象::成员方法
  • 范例:
"HelloWorld"::toUpperCase
  • String类中的方法:public String toUpperCase() 将此String所有字符转换为大写

引用类的实例对象

  • 引用类的实例对象,其实就是引用类中的成员方法
  • 格式:
类名::成员方法
  • 范例:
String::substring
  • String类中的方法:public String substring(int beginIndex, int endIndex) 从beginIndex开始到endIndex结束,截取字符串,返回一个子串,子串的长度为endIndex - beginIndex

引用构造器

  • 引用构造器,其实就是引用构造方法
  • 格式:
类名::new
  • 范例:
Student::new

函数式接口

函数式接口概述

  • 函数式接口:有且仅有一个抽象方法的接口
  • Java中的函数式编程体现就是Lambda表达式,所以函数式接口就是可以适用于Lambda使用的接口
  • 只有确保接口中有且仅有一个抽象方法,Java中的Lambda才能顺利地进行推导

如何检测一个接口是否是函数式接口?

  • @FunctionalInterface
  • 放在定义接口的上方,如果接口是函数式接口,编译通过;如果不是,编译失败

注意:

  • 我们自己定义函数式接口的时候,@FunctionalInterface是可选的,就算不写这个注解,只要保证满足函数式接口的定义条件,也照样是函数式接口。但是,建议加上该注解

函数式接口作为方法的参数

  • 如果方法的参数是一个函数式接口,可以使用Lambda表达式作为参数传递
start() -> System.out.println(Thread.currentThread().getName() + "线程启动了");

函数式接口作为方法的返回值

  • 如果方法的返回值是一个函数式接口,可以使用Lambda表达式作为结果返回
private static Comparator<String> getComparator() {
    return (s1, s2) -> s1.length() - s2.length();
}

常用的函数式接口

  • Supplier接口
  • Consumer接口
  • Predicate接口
  • Function接口

Supplier接口

  • Supplier<T>:包含一个无参的方法
  • T get():获得结果
  • 该方法不需要参数,它会按照某种实现逻辑由Lambda表达式(实现)返回一个数据
  • Supplier<T>接口也被称为生产型接口,如果我们指定了接口的泛型是什么类型,那么接口中的get()方法就会产生什么类型的数据供我们使用
Supplier接口练习之获取最大值
public class SupplierTest {
    public static void main(String[] args) {
        // 定义一个int数组
        int[] arr = {19, 50, 28, 46};

        int maxValue = getMax(() -> {
            int max = arr[0];

            for (int i = 1; i < arr.length; i++) {
                if (arr[i] > max) {
                    max = arr[i];
                }
            }

            return max;
        });

        System.out.println(maxValue);
    }

    // 返回一个int数组中的最大值
    public static int getMax(Supplier<Integer> sup) {
        return sup.get();
    }
}

Consumer接口

  • Consumer<T>:包含两个方法
  • void accept(T t):对给定的参数执行此操作
  • defaultConsumer<T> andThen(Consumer after):返回一个组合的Consumer,依次执行此操作,然后执行Then操作
  • Consumer<T>接口也被称为消费型接口,他消费的数据的数据类型由泛型指定
Consumer接口之按照要求打印信息
public class ConsumerTest {
    public static void main(String[] args) {
        String[] strArray = {"林青霞,30", "张曼玉,35", "王祖贤,33"};

        printInfo(strArray, str -> {
            String name = str.split(",")[0];
            System.out.print("姓名:" + name);
        }, str -> {
            int age = Integer.parseInt(str.split(",")[1]);
            System.out.println(",年龄:" + age);
        });
    }

    private static void printInfo(String[] array, Consumer<String> con1, Consumer<String> con2) {
        for (String s : array) {
            con1.andThen(con2).accept(s);
        }
    }
}

Predicate接口

  • Predicate<T>:常用的四个方法
  • boolean test(T t):对给定的参数进行判断(判断逻辑由Lambda表达式实现),返回一个布尔值
  • default Predicate<T> negate():返回一个逻辑的否定,对应逻辑非
  • default Predicate<T> and(Predicate other):返回一个组合判断,对应短路与
  • default Predicate<T> or(Predicate other):返回一个组合判断,对应短路或
  • Predicate<T>接口通常用于判断参数是否满足指定的条件
Predicate接口之筛选满足条件的数据
  • String[] strArray = {“林青霞,30”, “柳岩,34”, “张曼玉,35”, “貂蝉,31”, “王祖贤,33”};
  • 字符串数组中有多条信息,请通过Predicate接口的拼装将符合要求的字符串筛选到集合ArrayList中,并遍历ArrayList集合
  • 同时满足如下要求:姓名长度大于2,年龄大于33
import java.util.ArrayList;
import java.util.function.Predicate;

public class PreducateTest {
    public static void main(String[] args) {
        String[] strArray = {"林青霞,30", "柳岩,34", "张曼玉,35", "貂蝉,31", "王祖贤,33"};

        ArrayList<String> array = myFilter(strArray, s -> s.split(",")[0].length() > 2, s -> Integer.parseInt(s.split(",")[1]) > 33);

        for (String str : array) {
            System.out.println(str);
        }
    }

    private static ArrayList<String> myFilter(String[] strArray, Predicate<String> pre1, Predicate<String> pre2) {
        ArrayList<String> array = new ArrayList<String>();

        // 遍历数组
        for (String str : strArray) {
            if (pre1.and(pre2).test(str)) {
                array.add(str);
            }
        }

        return array;
    }
}

Function接口

  • Function<T, R>:常用的两个方法
  • R apply(T t):将此函数应用于给定的参数
  • deafult <V> Function andThen(Function after):返回一个组合函数,首先将该函数应用于输入,然后将after函数应用于结果
  • Function<T, R>接口通常用于对参数进行处理,转换(处理逻辑由Lambda表达式实现),然后返回一个新的值
Function接口之按照指定要求操作数据
import java.util.function.Function;

public class FunctionTest {
    public static void main(String[] args) {
        String s = "林青霞,30";

        // convert(s, ss -> ss.split(",")[1], ss -> Integer.parseInt(ss), i -> i + 70);
        convert(s, ss -> ss.split(",")[1], Integer::parseInt, i -> i + 70);
    }

    private static void convert(String s, Function<String, String> fun1, Function<String, Integer> fun2, Function<Integer, Integer> fun3) {
        int i = fun1.andThen(fun2).andThen(fun3).apply(s);
        System.out.println(i);
    }
}

Stream流

Stream流的生成方式

Stream流的使用

  • 生成流
    • 通过数据源(集合、数组等)生成流
    • list.stream()
  • 中间操作
    • 一个流后面可以跟零个或多个中间操作,其目的主要是打开流,做出某种程度的数据过滤/映射,然后返回一个新的流,交给下一个操作使用
    • filter()
  • 终结操作
    • 一个流只能有一个终结操作,当这个操作执行后,流就被使用“光”了,无法再被操作,所以这必定是流的最后一个操作
    • forEach()

Stream流常见生成方式

  • Collection体系的集合可以使用默认方法stream()生成流
    • default Stream<E> stream()
  • Map体系的集合间接的生成流
  • 数组可以通过Stream接口的静态方法of(T... values)生成流

Stream流中常见的中间操作

  • Stream<T> filter(Predicate predicate):用于对流中的数据进行过滤
  • Stream<T> limit(long maxSize):返回此流中的元素组成的流,截取前指定参数个数的数据
  • Stream<T> skip(long n):跳过指定参数个数的数据,返回由该流的剩余参数组成的流
  • static <T> Stream<T> concat(Stream a, Stream b):合并a和b两个流为一个流
  • Stream<T> distinct():返回由该流的不同元素(根据Objectequals(Object))组成的流
  • Stream<T> sorted():返回由此流的元素组成的流,根据自然顺序进行排序
  • Stream<T> sorted(Comparator):返回由该流的元素组成的流,根据提供的Comparator进行排序
  • <R> Stream<R> map(Function mapper):返回由给定的函数应用于此流的元素的结果组成的流
  • IntStream mapToInt(ToIntFunction mapper):返回一个IntStream其中包含将给定函数应用于此流的元素的结果

Stream流的常见终结操作方法

  • void forEach(Consumer action):对此流的每个元素执行操作
  • long count():返回此流中的元素数

Stream流的收集操作

  • R collect(Collector collector)
  • 但是这个收集方法的参数是一个Collector接口

工具类Collectors提供了具体的收集方法

  • public static <T> Collector toList():把元素收集到List集合中
  • public static <T> Collector toSet():把元素收集到Set集合中
  • public static Collector toMap(Function keyMapper, Function valueMapper):把元素收集到Map集合中

反射

类加载器

类加载

当程序要使用某个类时,如果该类还未被加载到内存中,则系统会通过类的加载、类的连接、类的初始化这三个步骤来对类进行初始化。如果不出现意外情况,JVM会连续完成这三个步骤,所以有时也把这三个步骤统称为类加载或类初始化。

类的加载

  • 就是将class文件读入内存,并为之创造一个java.lang.Class对象
  • 任何类被使用时,系统都会为之建立一个java.lang.Class对象

类的连接

  • 验证阶段:用于检验被加载的类是否有正确的内部结构,并和其他类协调一致
  • 准备阶段:负责为类的类变量分配内存,并设置默认初始化值
  • 解析阶段:将类的二进制数据中的符号引用替换为直接引用

类的初始化

  • 在该阶段,主要就是对类变量进行初始化

类的初始化步骤

  • 假如类还未被加载和连接,则程序先加载并连接该类
  • 假如类的直接父类还未被初始化,则先初始化其直接父类
  • 假如类中有初始化语句,则系统依次执行这些初始化语句

注意:在执行第二个步骤的时候,系统对直接父类的初始化步骤也是初始化步骤1-3

类的初始化时机

  • 创建类的实例
  • 调用类的方法
  • 访问类或者接口的类变量,或者为该类变量赋值
  • 使用反射方式来强制创建某个类或接口对应的java.lang.Class对象
  • 初始化某个类的子类
  • 直接使用java.exe命令来运行整个主类

类加载器

类加载器的作用

  • 负责将.class文件加载到内存中,并为之生成对应的java.lang.Class对象
  • 虽然不用过分关心类加载机制,但了解这个机制就能更好地理解程序的运行

JVM的类加载机制

  • 全盘负责:就是当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另一个类加载器来载入
  • 父类委托:就是当一个类加载器负责加载某个Class时,先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
  • 缓存机制:保证所有被加载过的Class都会被缓存,当程序需要使用某个Class对象时,类加载器先从缓存区中搜索该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存储到缓存区

Java运行时具有以下内置类加载器

  • Bootstrap class loader:它是虚拟机的内置类加载器,通常表示为null,并且没有父null
  • Platform class loader:平台类加载器可以看到所有平台类,平台类包括由平台类加载器或其祖先定义的JavaSE平台API,其实现类和JDK特有的运行时类
  • Systen class loader:它也被称为应用程序类加载器,与平台类加载器不同,系统类加载器通常用于定义应用程序类路径、模块路径和JDK特定工具上的类
  • 类加载器的继承关系:System的父加载器为Platform,而Platform的父加载器为Bootstrap

ClassLoader 中的两个方法

  • static ClassLoader getSystemClassLoader():返回用于委派的系统类加载器
  • ClassLoader getParent():返回父类加载器进行委派

反射

反射概述

Java反射机制:是指在运行时去获取一个类的变量和方法信息,然后通过获取到的信息来创建对象,调用方法的一种机制。由于这种动态性,可以极大地增强程序的灵活性,程序不用在编译期就完成确定,在运行期仍然可以扩展。

获取Class类的对象

  • 实用类的class属性来获取该类对应的Class对象
    • 举例:Student.class将会返回Student类对应的Class对象
    • 基本数据类型也可以通过.class得到对应的Class属性
  • 调用对象的getClass()方法,返回对象所属类对应的Class对象
    • 该方法是Object类中的方法,所有的Java对象都可以调用该方法
  • 使用Class类中的静态方法forName(String className),该方法需要传入字符串参数,该字符串参数的值是某个类的全路径,也就是完整包名的路径

反射获取构造方法并使用

Class类中用于获取构造方法的方法

  • Constructor<?>[] getConstructors():返回所有公共构造方法对象的数组
  • Constructor<?>[] getDeclaredConstructors():返回所有构造方法对象的数组
  • Constructor<T> getConstructor(Class<?>... parameterTypes):返回单个公共构造方法对象
  • Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes):返回单个构造方法对象

Constructor类中创建对象的方法

  • T newInstance(Object... initargs):根据指定的构造方法创建对象

反射获取成员变量并使用

  • Field[] getFields():返回所有公共成员变量对象的数组
  • Field[] getDeclaredFields():返回所有成员变量对象的数组
  • Field getField(String name):返回单个公共成员变量对象
  • Field getDeclaredField(String name):返回单个成员变量对象

Field类中用于给成员变量赋值的方法

  • void set(Object obj, Object value):给Object对象的成员变量赋值为value

反射获取成员方法并使用

  • Method[] getMethods():返回所有公共成员方法对象的数组,不包括继承的
  • Method[] getDeclaredMethods():返回所有成员方法对象的数组,不包括继承的
  • Method getMethod(String name, Class<?>... parameterTypes):返回单个公共成员方法对象
  • Method getDeclaredMethod(String name, Class<?>... parameterTypes):返回单个成员方法对象

Methos类中用于调用成员方法的对象

  • Object invoke(Object obj, Object... args):调用obj对象的成员方法,参数是args,返回值是Object类型

反射练习

练习1:有一个ArrayList集合,现在想在这个集合中添加一个字符串数据,如何实现?

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;

public class ReflectTest {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        // 创建集合
        ArrayList<Integer> array = new ArrayList<Integer>();

        Class<? extends ArrayList> c = array.getClass();
        Method m = c.getMethod("add", Object.class);
        m.invoke(array, "hello");
        m.invoke(array, "world");
        m.invoke(array, "java");

        System.out.println(array);
    }
}

练习2:通过配置文件运行类中的方法

import java.io.FileReader;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Properties;

public class ReflectTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        // 加载数据
        Properties prop = new Properties();
        FileReader fr = new FileReader("myReflect\\class.txt");
        prop.load(fr);
        fr.close();

        String className = prop.getProperty("className");
        String methodMame = prop.getProperty("methodMame");

        // 通过反射来使用
        Class<?> c = Class.forName(className);

        Constructor<?> con = c.getConstructor();
        Object obj = con.newInstance();

        Method m = c.getMethod(methodMame);
        m.invoke(obj);
    }
}

class.txt

className=com.itheima_06.Teacher
methodName=teach

模块化

模块的基本使用

模块的基本使用步骤

  • 创建模块(按照以前讲解的方式创建模块,创建包、创建类、定义方法)
  • 在模块的src目录下新建一个名为module-info.java的描述性文件,该文件专门定义模块名、访问权限、模块依赖等信息
    • 描述性文件中使用模块导出和模块依赖来进行配置并使用
  • 模块中所有未导出的包都是模块私有的,它们是不能在模块之外被访问的
    • 模块导出格式:export 包名;
  • 一个模块要访问其他模块,必须明确指定依赖哪些模块,未明确指定依赖的模块不能访问
    • 模块依赖格式:requires 模块名;
    • 注意:写模块名报错,需要按下ALT+ENTER提示,然后选择模块依赖

模块服务的使用

  • 模块导出:export com.itheima_03;
  • 服务提供:provides MyService with Itheima; 指定MyService的服务实现类是Itheima
  • 声明服务接口:uses MyService;
  • 使用接口提供的服务
    • ServiceLoader:一种加载服务实现的工具
    • ServiceLoader<MyService> myServices = ServiceLoader.load(MyService.class);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值