Java从入门到精通(三)

简介: Java从入门到精通(三)

Java从入门到精通(三)


JDK8 中新的日期时间 API

日期时间 API 的迭代

第一代:JDK1.0 Date 类

第二代:JDK1.1 Calendar 类,一定程度上替代了 Date 类

第三代:JDK1.8 提出了一套新的 API

前两代存在的问题举例

可变性:像日期和时间这样的类应该是不可变的。

偏移性:Date中的年份是从1900开始的,而月份都从0开始。

格式化:格式化只对Date有用,Calendar则不行。

此外,它们也不是线程安全的;不能处理闰秒等。

JDK8 中新的日期时间 API涉及到的包

java.time:包含值对象的基础包

java.time.chrono:提供对不同的日历系统的访问

java.time.format:格式化和解析时间和日期

java.time.temporal:包括底层框架和扩展特性

java.time.zone:包含时区支持的类

说明:大多数开发者只会用到基础包和format包,也可能会用到temporal包。因此,尽管有68个新的公开类型,大多数开发者,大概将只会用到其中的三分之一。

LocalDate、LocalTime、LocalDateTime

说明

分别表示使用 ISO-8601日历系统的日期、时间、日期和时间。 它们提供了简单的本地日期或时间,并不包含当前的时间信息,也不包含与时区 相关的信息。

LocalDateTime 相较于 LocalDate、LocalTime,使用频率要高

类似于 java.util.Date 类

常用方法

image.pngimage.png

时间点:Instant

说明

时间线上的一个瞬时点,概念上讲,它只是简单的表示自1970年1月1日0时0分0秒(UTC)开始的毫秒数

类似于 java.util.Date 类

常用方法

image.png

时间戳是指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数。

DateTimeFormatter

说明

格式化或解析日期,时间

类似于 SimpleDateFormat

常用方法

实例化方式

预定义的标准格式。

ISO_LOCAL_DATE_TIME

ISO_LOCAL_DATE

ISO_LOCAL_TIME

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

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

常用方法

image.png

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

// 方式三:自定义的格式。如:ofPattern(“yyyy-MM-dd hh:mm:ss”)
DateTimeFormatter formatter3 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
System.out.println(formatter3.format(localDateTime));
TemporalAccessor accessor = formatter3.parse("2021-11-25 21:53:44");
System.out.println(accessor);

其它 API 的使用

带时区的日期时间:ZonedDateTime / ZoneId

/**
 * ZoneId:类中包含了所有时区信息
 */
@Test
public void test4(){
    // getAvailableZoneIds():获取对应的 ZoneId
    Set<String> zoneIds = ZoneId.getAvailableZoneIds();
    for (String zoneId : zoneIds) {
        System.out.println(zoneId);
    }
    System.out.println();
    // 获取"Asia/Tokyo"时区对应的时间
    System.out.println(LocalDateTime.now(ZoneId.of("Asia/Tokyo")));
}
/**
 * ZonedDateTime:带时区的日期时间
 */
@Test
public void test5(){
    // now():获取本时区的 ZonedDateTime 对象
    System.out.println(ZonedDateTime.now());
    // now(ZoneId id):获取指定时区的 ZonedDateTime 对象
    ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
}

时间间隔:Duration

用于计算两个“时间”间隔,以秒和纳秒为基准

image.png

@Test
public void test6(){
    //Duration:用于计算两个“时间”间隔,以秒和纳秒为基准  
    LocalTime localTime = LocalTime.now();
    LocalTime localTime1 = LocalTime.of(15, 23, 32);
    //between():静态方法,返回Duration对象,表示两个时间的间隔  
    Duration duration = Duration.between(localTime1, localTime);      
    System.out.println(duration);
    System.out.println(duration.getSeconds()); 
    System.out.println(duration.getNano());
    LocalDateTime localDateTime = LocalDateTime.of(2016, 6, 12, 15, 23, 32);
    LocalDateTime localDateTime1 = LocalDateTime.of(2017, 6, 12, 15, 23, 32);
    Duration duration1 = Duration.between(localDateTime1, localDateTime);  
    System.out.println(duration1.toDays());
}

时间间隔:Period

用于计算两个“日期”间隔,以年、月、日衡量

image.png

@Test
public void test7(){
    LocalDate localDate = LocalDate.now();
    LocalDate localDate1 = LocalDate.of(2028, 3, 18);
    Period period = Period.between(localDate, localDate1);  
    System.out.println(period);
    System.out.println(period.getYears()); 
    System.out.println(period.getMonths());
    System.out.println(period.getDays());
    Period period1 = period.withYears(2);  
    System.out.println(period1);
}

时间校正器:TemporalAdjuster

@Test
public void test8(){
    // 获取当前日期的下一个周日是哪天?
    TemporalAdjuster temporalAdjuster = TemporalAdjusters.next(DayOfWeek.SUNDAY);  
    LocalDateTime localDateTime = LocalDateTime.now().with(temporalAdjuster);  
    System.out.println(localDateTime);
    // 获取下一个工作日是哪天?
    LocalDate localDate = LocalDate.now().with(new TemporalAdjuster() {
        @Override
        public Temporal adjustInto(Temporal temporal) {
            LocalDate date = (LocalDate) temporal;
            if (date.getDayOfWeek().equals(DayOfWeek.FRIDAY)) {
                return date.plusDays(3);
            } else if (date.getDayOfWeek().equals(DayOfWeek.SATURDAY)) {  
                return date.plusDays(2);
            } else {
                return date.plusDays(1);
            }
        }
    });
    System.out.println("下一个工作日是:" + localDate);
}

Java比较器

使用背景

Java 中的对象,正常情况下,只能进行比较:== 或 != ,不能使用 > 或 < 的,但是在开发场景中,需要对多个对象进行排序,言外之意,就需要比较对象的大小,如何实现?使用两个接口中的任何一个:Comparable 或 Comparator

自然排序:使用 Comparable 接口

说明

像 String、包装类等实现了 Comparable 接口,重写了 compareTo(obj) 方法,给出了比较两个对象大小的方式

像 String、包装类等重写了 compareTo(obj) 方法以后,进行了从小到大的规则

重写 compareTo(obj) 的规则:

如果当前对象 this 大于形参对象 obj,则返回正整数

如果当前对象 this 小于形参对象 obj,则返回负整数

如果当前对象 this 等于形参对象 obj,则返回零

对于自定义类来说,如果需要排序,可以自定义类实现 Comparable 接口,重写 compareTo(obj) 方法,在 compareTo(obj) 方法中指明如何排序

自定义类代码举例

public class Goods implements Comparable{
    private String name;
    private double price;
    public Goods() {
    }
    public Goods(String name, double price) {
        this.name = name;
        this.price = price;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public double getPrice() {
        return price;
    }
    public void setPrice(double price) {
        this.price = price;
    }
    @Override
    public String toString() {
        return "Goods{" +
                "name='" + name + '\'' +
                ", price=" + price +
                '}';
    }
    /**
     * 指明商品比较大小的方式:按照价格从低到高排序,再按照产品名称从低到高排序
     * @param o
     * @return
     */
    @Override
    public int compareTo(Object o) {
        if (o instanceof Goods){
            Goods goods = (Goods) o;
            // 方式一
            if (this.price > goods.price){
                return 1;
            }else if (this.price < goods.price){
                return -1;
            }else {
                return this.name.compareTo(goods.name);
            }
            // 方式二
            // return Double.compare(this.price, goods.price);
        }
        throw new RuntimeException("传入的数据类型不一致");
    }
}

定制排序:使用 Comparator 接口

说明

背景

当元素的类型没有实现 java.lang.Comparable 接口而又不方便修改代码,或者实现了java.lang.Comparable 接口的排序规则不适合当前的操作,那么可以考虑使用 Comparator 的对象来排序

重写 compare(Object o1,Object o2) 方法,比较 o1 和 o2 的大小:

如果返回正整数,表示 o1 大于 o2;

如果返回0,表示相等;

如果返回负整数,表示 o1 小于 o2

代码举例

Comparator comparator = new Comparator() {
    /**
     * 按照字符串从大到小排序
     * @param o1
     * @param o2
     * @return
     */
    @Override
    public int compare(Object o1, Object o2) {
        if (o1 instanceof String && o2 instanceof  String){
            String s1 = (String) o1;
            String s2 = (String) o2;
            return -s1.compareTo(s2);
        }
        throw new RuntimeException("输入的数据类型不一致");
    }
};

两种排序方式对比

Comparable 接口的方式一旦一定,保证 Comparable 接口实现类的对象在任何位置都可以比较大小

Comparator 接口属于临时性的比较

其它类

System 类

System类代表系统,系统级的很多属性和控制方法都放置在该类的内部。 该类位于java.lang包。

由于该类的构造器是private的,所以无法创建该类的对象,也就是无法实 例化该类。其内部的成员变量和成员方法都是static的,所以也可以很方便 的进行调用。

方法

native long currentTimeMillis():该方法的作用是返回当前的计算机时间,时间的表达格式为当前计算机时间和GMT时间(格林威治时间)1970年1月1号0时0分0秒所差的毫秒数。

void exit(int status): 该方法的作用是退出程序。其中status的值为0代表正常退出,非零代表 异常退出。使用该方法可以在图形界面编程中实现程序的退出功能等。

void gc():该方法的作用是请求系统进行垃圾回收。至于系统是否立刻回收,则 取决于系统中垃圾回收算法的实现以及系统执行时的情况。

String getProperty(String key):该方法的作用是获得系统中属性名为key的属性对应的值。系统中常见的属性名以及属性的作用如下表所示:

image.png

Math 类

java.lang.Math 提供了一系列静态方法用于科学计算。其方法的参数和返回值类型一般为 double 型。

BigInteger 类、BigDecimal 类

说明:

java.math 包的 BigInteger 可以表示不可变的任意精度的整数。

要求数字精度比较高,故用到 java.math.BigDecimal 类

代码举例:

@Test
public void testBigInteger() {
    BigInteger bi = new BigInteger("12433241123");
    BigDecimal bd = new BigDecimal("12435.351");
    BigDecimal bd2 = new BigDecimal("11");
    System.out.println(bi);
    // System.out.println(bd.divide(bd2));
    System.out.println(bd.divide(bd2, BigDecimal.ROUND_HALF_UP));
    System.out.println(bd.divide(bd2, 25, BigDecimal.ROUND_HALF_UP));
}

枚举类和注解

枚举类

枚举类的声明

枚举类的理解:类的对象只有有限个,确定的,称此类为枚举类

当需要定义一组常量时,强烈建议使用枚举类

如果枚举类中只有一个对象,则可以作为单例模式的实现方式

如何自定义枚举类

JDK5.0之前,自定义枚举类

// 自定义枚举类
class Season{
    // 1.声明 Season 对象的属性,private final 修饰
    private final String seasonName;
    private final String seasonDesc;
    // 2.私有化类的构造器
    private Season(String seasonName, String seasonDesc){
        this.seasonName = seasonName;
        this.seasonDesc = seasonDesc;
    }
    // 3.提供当前枚举类的多个对象:public static final 的
    public static final Season SPRING = new Season("春天", "春暖花开");
    public static final Season SUMMER = new Season("夏天", "夏日炎炎");
    public static final Season AUTUMN = new Season("秋天", "秋高气爽");
    public static final Season WINTER = new Season("冬天", "冰天雪地");
    // 4.其它诉求1:获取枚举类对象的属性
    public String getSeasonName() {
        return seasonName;
    }
    public String getSeasonDesc() {
        return seasonDesc;
    }
    // 4.其它诉求2:提供 toString()
    @Override
    public String toString() {
        return "Season{" +
                "seasonName='" + seasonName + '\'' +
                ", seasonDesc='" + seasonDesc + '\'' +
                '}';
    }
}

JDK5.0,新增使用 enum 关键字定义枚举类

// 使用 enum 关键字定义枚举类
enum Season1{
    // 1.提供当前枚举类的对象,多个对象之间用逗号隔开
    SPRING("春天", "春暖花开").
    SUMMER("夏天", "夏日炎炎"),
    AUTUMN("秋天", "秋高气爽"),
    WINTER("冬天", "冰天雪地");
    // 2.声明 Season 对象的属性,private final 修饰
    private final String seasonName;
    private final String seasonDesc;
    // 3.私有化类的构造器
    Season1(String seasonName, String seasonDesc){
        this.seasonName = seasonName;
        this.seasonDesc = seasonDesc;
    }
    // 4.其它诉求1:获取枚举类对象的属性
    public String getSeasonName() {
        return seasonName;
    }
    public String getSeasonDesc() {
        return seasonDesc;
    }
}

枚举类中的常用方法

继承于 java.lang.Enum 类

values():返回枚举类型的对象数组。该方法可以很方便地遍历所有的枚举值。

valueOf(String str):可以把一个字符串转为对应的枚举类对象。要求字符串必须是枚举类对象的“名字”。如不是,会有运行时异常:IllegalArgumentException。

toString():返回当前枚举类对象常量的名称

public static void main(String[] args) {
    Season1 summer = Season1.SUMMER;
    System.out.println(summer);
    System.out.println(Season1.class.getSuperclass());
    // values()
    Season1[] values = Season1.values();
    for (Season1 value : values) {
        System.out.println(value);
    }
    for (Thread.State value : Thread.State.values()) {
        System.out.println(value);
    }
    // valueOf(String objName):返回枚举类中对象名是 objName 的对象
    // 如果没有 objName 的枚举类对象,则抛异常 IllegalArgumentsException
    Season1 winter = Season1.valueOf("WINTER");
    System.out.println(winter);
}

使用 enum 关键字定义的枚举类实现接口的情况

情况一:实现接口,在 enum 类中实现抽象方法

情况二:让枚举类对象分别实现接口中的抽象方法

interface Info{
    void show();
}
// 使用 enum 关键字定义枚举类
enum Season1 implements Info{
    // 1.提供当前枚举类的对象,多个对象之间用逗号隔开
    SPRING("春天", "春暖花开"){
        @Override
        public void show() {
            System.out.println("春天在哪里");
        }
    },
    SUMMER("夏天", "夏日炎炎") {
        @Override
        public void show() {
            System.out.println("宁夏");
        }
    },
    AUTUMN("秋天", "秋高气爽") {
        @Override
        public void show() {
            System.out.println("秋天不回来");
        }
    },
    WINTER("冬天", "冰天雪地") {
        @Override
        public void show() {
            System.out.println("大约在冬季");
        }
    };
    // 2.声明 Season 对象的属性,private final 修饰
    private final String seasonName;
    private final String seasonDesc;
    // 3.私有化类的构造器
    Season1(String seasonName, String seasonDesc){
        this.seasonName = seasonName;
        this.seasonDesc = seasonDesc;
    }
    // 4.其它诉求1:获取枚举类对象的属性
    public String getSeasonName() {
        return seasonName;
    }
    public String getSeasonDesc() {
        return seasonDesc;
    }
}

注解

理解 Annotation

JDK5.0新增

Annotation 其实就是代码里的特殊标记, 这些标记可以在编译, 类加载,运行时被读取,并执行相应的处理,通过使用 Annotation, 程序员可以在不改变原有逻辑的情况下, 在源文件中嵌入一些补充信息

在 JavaSE 中,注解的使用目的比较简单,例如标记过时的功能,忽略警告等。在 JavaEE / Android 中注解占据了更重要的角色,例如用来配置应用程序的任何切面,代替 JavaEE 旧版中所遗留的繁冗代码和 XML 配置等

框架 = 注解 + 反射机制 + 设计模式

Annotation的使用实例

示例1:生成文档相关的注解

示例2:在编译时进行格式检查(JDK 内置三个基本注解)

@Override: 限定重写父类方法, 该注解只能用于方法

@Deprecated: 用于表示所修饰的元素(类, 方法等)已过时。通常是因为所修饰的结构危险或存在更好的选择

@SuppressWarnings: 抑制编译器警告

示例3:跟踪代码依赖性,实现替代配置文件功能

如何自定义注解

参照 @SuppressWarings 定义

步骤

注解声明为 @interface

内部定义成员,通常使用 value 表示

可以指定成员的默认值,使用 default 定义

如果自定义注解没有成员,表明是一个标识作用

代码举例

@Inherited
@Repeatable(MyAnnotations.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({
        ElementType.TYPE,
        ElementType.FIELD,
        ElementType.METHOD,
        ElementType.LOCAL_VARIABLE,
        ElementType.TYPE_PARAMETER,
        ElementType.TYPE_USE
})
public @interface MyAnnotation {
    String value() default "hello";
}

说明:

如果注解有成员,在使用注解时,需要指明成员的值

自定义注解必须配上注解的信息处理流程(使用反射)才有意义

自定义注解通常都会指明两个元注解:Rentention Target

元注解

元注解:对现有的注解进行解释说明的注解

Retention:指定所修饰的 Annotation 的生命周期:SOURCE CLASS(默认) RUNTIME,只有声明为 RUNTIME 生命周期的注解,才能提供反射获取

Target:用于指定被修饰的 Annotation 能用于修饰哪些程序元素

Documented:表示所修饰的注解在 Javadoc 解析时,保留下来

Inherited:被它修饰的 Annotation 将具有继承性

如何获取注解信息

通过反射获取注解信息

前提:要求此注解的元注解 Retention 中声明的生命周期状态为:RUNTIME

JDK8 中注解的新特性

可重复注解

在 MyAnnotation 上声明 @Repeatable,成员值为 MyAnnotations.class

MyAnnotation 的 Target 和 Retention等元注解必须和 MyAnnotations 相同

类型注解

ElementType.TYPE_PARAMETER 表示该注解能写在类型变量的声明语句中(如:泛型声明)。

ElementType.TYPE_USE 表示该注解能写在使用类型的任何语句中

集合

数组与集合

集合框架的概述

集合、数组都是对多个数据进行存储操作的结构,简称 Java 容器

说明:此时的存储,主要指的是内存层面的存储,不涉及到持久化的存储(.txt, .jpg, .avi,数据库中)

数组在存储的特点

一旦初始化以后,其长度就确定了

数组一旦定义好,其元素的类型也就确定了,也就只能操作指定类型的数据了

比如:String[] arr int[] arr;

数据存储的弊端

一旦初始化以后,其长度就不可修改

数组中提供方法非常有限,对于添加、删除、插入数据等操作,非常不便

获取数组中实际元素的个数的需求,数组没有现成的属性或方法可用

数组存储数据的特点:有序、可重复,对于无序、不可重复的需求,数组不能满足

集合存储的优点

解决数组存储数据方面的弊端

集合框架

|---- Collection 接口:单列集合,用来存储一个一个的数据
      |---- List 接口:存储有序的、可重复的数据 --> “动态"数组
            |---- ArrayList
            |---- LinkedList
            |---- Vector
      |---- Set 接口:存储无序的、不可重复的数据 --> 高中讲的"集合“
            |---- HashSet
                  |---- LinkedHashSet
            |---- TreeSet
|---- Map 接口:双列集合,用来存储一对(key : value)一对的数据 --> 高中函数: y = f(x)
      |---- HashMap
            |---- LinkedHashMap
      |---- TreeMap
      |---- HashTable
      |---- Properties

Collection 接口

单列集合框架结构
|---- Collection 接口:单列集合,用来存储一个一个的数据
      |---- List 接口:存储有序的、可重复的数据 --> “动态"数组
            |---- ArrayList
            |---- LinkedList
            |---- Vector
      |---- Set 接口:存储无序的、不可重复的数据 --> 高中讲的"集合“
            |---- HashSet
                    |---- LinkedHashSet
            |---- TreeSet

对应图示

Collection 接口的常用方法

add(Object obj):将元素 obj 添加到元素集合中

addAll(Collection collection):将 collection 集合中的元素添加到当前的集合中

size():获取添加的元素的个数

isEmpty():判断当前集合是否为空

clear():清空集合元素

contains(Object obj):判断当前集合中是否包含 obj,在判断时会调用 obj 对象所在类的 equals()

containsAll(Collection collection):判断形参 collection 中的所有元素是否都存在于当前集合中

remove(Object obj):从当前集合中移除 obj 元素

removeAll(Collection collection):差集,从当前集合中移除 collection 中所有的元素

retainsAll(Collection collection):交集,获取当前集合和 collection 集合的交集,并返回给当前集合

equals(Object obj):要想返回 true,就要判断当前集合和形参集合元素都相同

hashCode():返回当前对象的哈希值

toArray():集合转换为数组

iterator()返回此集合中的元素的迭代器

Collection 集合与数组之间的转换

// 8.toArray():集合转换为数组
Object[] arr = collection.toArray();
for (int i = 0; i < arr.length; i++) {
    System.out.println(arr[i]);
}
// 扩展:数组转换为集合:调用 Arrays 类的静态方法 asList()
List<String> list = Arrays.asList(new String[] {"aa", "bb", "cc"});
System.out.println(list);

使用 Collection 集合存储对象,要求对象所属的类满足

向 Collection 接口的实现类的对象中添加数据 obj 时,要求 obj 所在类要重写 equals()

要求

层次一:选择合适的集合类去实现数据的保存,调用其内部的相关方法

层次二:不同的集合类底层的数据结构为何?如何实现数据的操作的:增删改查等

Iterator 接口与 foreach 循环

遍历 Collection 的两种方式

使用迭代器 Iterator

foreach 循环(或增强 for 循环)

java.utils 包下定义的迭代器接口:Iterator

说明

Iterator对象称为迭代器(设计模式的一种),主要用于遍历 Collection 集合中的元素。

GOF给迭代器模式的定义为:提供一种方法访问一个容器(container)对象中各个元 素,而又不需暴露该对象的内部细节。迭代器模式,就是为容器而生。

内部方法:hasNext() next()

集合对象每次调用 iterator() 方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前。

内部定义了 remove(),可以在遍历的时候,删除集合中的元素,此方法不同于集合直接调用 remove()

作用

遍历集合 Collection 元素

如何获取实例

collection.iterator() 返回一个迭代器实例

遍历的代码实现

Iterator iterator = collection.iterator();
// hasNext():判断是否会有下一个元素
while (iterator.hasNext()){
    // next():指针下移;将下移以后集合位置上的元素返回
    System.out.println(iterator.next());
}
图示说明

remove() 的使用
// 如果还未调用 next() 或在上一次调用 next 方法之后已经调用了 remove 方法,
// 再调用 remove 都会报 IllegalStateException。
// 内部定义了 remove(),可以在遍历的时候,删除集合中的元素,此方法不同于集合直接调用 remove()
@Test
public void test3() {
    Collection collection = new ArrayList();
    collection.add(123);
    collection.add(456);
    collection.add(new Person("Jerry", 20));
    collection.add(new String("Tom"));
    collection.add(false);
    Iterator iterator = collection.iterator();
    // 删除集合中 “Tom” 数据
    while (iterator.hasNext()){
        Object obj = iterator.next();
        if ("Tom".equals(obj)){
            iterator.remove();
        }
    }
    iterator = collection.iterator();
    while (iterator.hasNext()){
        System.out.println(iterator.next());
    }
}

JDK5 新特性 – 增强 for 循环:(foreach 循环)

遍历集合举例

@Test
public void test1() {
    Collection collection = new ArrayList();
    collection.add(123);
    collection.add(456);
    collection.add(new Person("Jerry", 20));
    collection.add(new String("Tom"));
    collection.add(false);
    // for(集合中元素的类型 局部变量 : 集合对象)
    // 内部仍然调用了迭代器
    for(Object obj : collection){
        System.out.println(obj);
    }
}

说明:内部仍然调用了迭代器

遍历数组举例

@Test
public void test2(){
    int[] arr = new int[]{1, 2, 3, 4, 5, 6};
    for (int a : arr){
        System.out.println(a);
    }
}

Collection 子接口:List 接口

存储的数据特点

存储有序的、可重复的数据

常用方法

增:add(Object obj)

删:remove(int index) / remove(Object obj)

改:set(int index, Object ele)

查:get(int index)

插:add(int index, Object ele)

长度:size()

遍历

Iterator 迭代器方式

增强 for 循环

普通 for 循环

常用实现类

|---- Collection 接口:单列集合,用来存储一个一个的数据
      |---- List 接口:存储有序的、可重复的数据 --> “动态"数组,替换原有数组
            |---- ArrayList
                     作为 List 接口的主要实现类
                     线程不安全,效率高
                     底层使用 Object[] elementData 存储
            |---- LinkedList
                     对于频繁的插入和删除,使用此类效率比 ArrayList 高
                     底层使用双向列表存储
            |---- Vector
                     作为 List 接口的古老实现类
                     线程安全,效率低
                     底层使用 Object[] elementData 存储

源码分析

ArrayList 的源码分析

JDK7 情况下

ArrayList list = new ArrayList(); // 底层创建了长度是10的 Object[] 数组 elementData

list.add(123); // elementData[0] = new Integer(123);

list.add(11); // 如果此次的添加导致底层 elementData 数组容量不够,则扩容,默认情况下,扩容为原来容量的1.5倍,同时需要将原有数组中的数据复制到新的数组中

结论:建议开发中使用带参的构造器:ArrayList list = ArrayList(int initialCapacity);

JDK8 中的变化

ArrayList list = new ArrayList(); // 底层 Object[] elementData = {},并没有创建长度为10的数组

list.add(123); // 第一次调用 add() 时,底层才创建了长度为10的数组,并将数据添加到 elementData 中

后续的添加和扩容操作与 JDK7 无异

小结

JDK7 中的 ArrayList 的对象创建类似于单例的饿汉式

JDK8 中的 ArrayList 的对象创建类似于单例的懒汉式,延迟了数组的创建,节省内存

LinkedList 的源码分析

LinkedList list = new LinkedList(); // 内部声明了 Node 类型的 first 和 last 属性,默认值为 null

list.add(123); // 将123封装到 Node 中,创建了 Node 对象

其中,Node 定义为:体现了 LinkedList 的双向链表的说法

private static class Node<E> {
      E item;
      Node<E> next;
      Node<E> prev;
      Node(Node<E> prev, E element, Node<E> next) {
          this.item = element;
          this.next = next;
          this.prev = prev;
      }
  }

Vector 的源码分析

通过 Vector() 构造器创建对象时,底层都创建了长度为10的数组,在扩容反码,默认扩容为原来数组长度的2倍

存储的元素的要求

添加的对象所在的类要重写 equals()

面试题:ArrayList、LinkedList、Vector 三者的异同?

同:三个类都是实现了 List 接口,存储数据的特点相同:存储有序的可重复的数据

ArrayList

作为 List 接口的主要实现类

线程不安全,效率高

底层使用 Object[] elementData 存储

LinkedList

对于频繁的插入和删除,使用此类效率比 ArrayList 高

底层使用双向列表存储

Vector

作为 List 接口的古老实现类

线程安全,效率低

底层使用 Object[] elementData 存储

Collection 子接口:Set 接口

存储数据的特点

无序性

不可重复性

具体的:

以 HashSet 为例说明

无序性:不等于随机性。存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值决定

不可重复性:保证添加的元素按照 equals() 判断时,不能返回 true,即:相同的元素只能添加一个

元素添加过程

以 HashSet 为例

向 HashSet 中添加元素 a,首先调用元素 a 所在类的 hashCode() 方法,计算元素 a 的哈希值,此哈希值接着通过某种算法计算出在 HashSet 底层数组中的存放位置(即为:索引位置),判断数组此位置上是否以及有元素

如果此位置上没有其它元素,则元素 a 添加成功 --> 情况1

如果此位置上有其它元素 b(或以链表形式存在多个元素),则比较元素 a 与元素 b 的哈希值

如果哈希值不相同,则元素 a 添加成功 --> 情况2

如果哈希值相同,进而需要调用元素 a 所在类的 equals() 方法

equals() 返回 true,元素 a 添加失败

equals() 返回 false,元素 a 添加成功 --> 情况3

对于添加成功的情况2和情况3而言:元素 a 与已经存在指定索引位置上数据以链表的方式存储

JDK7:元素 a 放到数组中,指向原来的元素

JDK8:原来的元素放到数组中,指向元素 a

总结:七上八下

常用方法

Set 接口中没有额外定义新的方法,使用的都是 Collection 中声明过的方法

常用实现类

|---- Collection 接口:单列集合,用来存储一个一个的数据
    |---- Set 接口:存储无序的、不可重复的数据 --> 高中讲的"集合“
          |---- HashSet
                    作为 Set 接口的主要实现类
                    线程不安全的
                    可以存储 null 值
               |---- LinkedHashSet
                         作为 HashSet 的子类,在添加数据的同时,每个数据还维护了两个引用,
                         记录此数据的前一个数据和后一个数据
                         遍历其内部数据时,可以按照添加的顺序遍历
                         对于频繁的遍历操作:LinkedHashSet 效率高于 HashSet
          |---- TreeSet
                    可以按照添加对象的指定属性,进行排序

TreeSet 的使用

使用说明

向 TreeSet 中添加的数据,要求是相同类的对象

两种排序方式:自然排序(Comparable) 定制排序(Compatator)

常用的排序方式

自然排序

@Test
public void test1(){
    TreeSet set = new TreeSet();
    // 失败:不能添加不同类的对象
    // set.add(456);
    // set.add(123);
    // set.add("AA");
    // set.add("CC");
    // set.add(new User("Tom", 12));
    // set.add(34);
    // set.add(-34);
    // set.add(43);
    // set.add(11);
    // set.add(8);
    set.add(new User("Tom", 12));
    set.add(new User("Jerry", 32));
    set.add(new User("Jim", 22));
    set.add(new User("Mike", 65));
    set.add(new User("Jack", 33));
    set.add(new User("Jack", 56));
    Iterator iterator = set.iterator();
    while (iterator.hasNext()) {
        System.out.println(iterator.next());
    }
}

定制排序

@Test
public void test2() {
    Comparator comparator = new Comparator() {
        /**
         * 按照年龄从小到大排列
         * @param o1
         * @param o2
         * @return
         */
        @Override
        public int compare(Object o1, Object o2) {
            if (o1 instanceof User && o2 instanceof User){
                User u1 = (User) o1;
                User u2 = (User) o2;
                return Integer.compare(u1.getAge(), u2.getAge());
            }
            throw new RuntimeException("输入的数据类型不匹配");
        }
    };
    TreeSet set = new TreeSet(comparator);
    set.add(new User("Tom", 12));
    set.add(new User("Jerry", 32));
    set.add(new User("Jim", 22));
    set.add(new User("Mike", 65));
    set.add(new User("Jack", 33));
    set.add(new User("Jack", 56));
    Iterator iterator = set.iterator();
    while (iterator.hasNext()) {
        System.out.println(iterator.next());
    }
}

存储的元素的要求

HashSet / LinkedHashSet

向 Set(HashSet / LinkedHashSet) 中添加的数据,其所在的类一定要重写 hashCode() 和 equals()

要求

重写的 hashCode() 和 equals() 尽可能保持一致性:相等的对象必须具有相等的散列码

重写两个方法的小技巧:对象中用作 equals() 方法比较的 Field,都应该用来计算 hashCode 值。

TreeSet

自然排序中,比较两个对象是否相同的标准为:compareTo() 返回0,不再是 equals()

定制排序中,比较两个对象是否相同的标准为:compare() 返回0,不再是 equals()

Map 接口

常用实现类结构

|---- Map:双列数据,存储 key-value 对的数据 --> 类似于高中函数:y = f(x)
      |---- HashMap
                作为 Map 的主要实现类
                线程不安全的,效率高
                存储 null 的 key 和 value
            |---- LinkedHashMap
                        保证在遍历 map 元素时,可以按照添加的顺序实现遍历
                        原因:在原有的 HashMap 底层结构基础上,添加了一对指针,指向前一个和后一个元素
                        对于频繁的遍历操作,此类执行效率高于 HashMap
      |---- TreeMap
                保证按照添加的 key-value 对进行排序,实现排序遍历,此时考虑 key 的自然排序和定制排序
                底层使用红黑树
      |---- Hashtable
                作为 Map 的古老实现类
                线程安全的,效率低
                不能存储 null 的 key 和 value
            |---- Properties
                      常用来处理配置文件
                      key 和 value 都是 String 类型

HashMap 的底层:

JDK7及之前:数组 + 链表

JDK8:数组 + 链表 + 红黑树

面试题:

HashMap 的底层实现原理

HashMap 和 Hashtable 的异同

CurrentHashMap 和 Hashtable 的异同(暂时不讲)

存储结构的理解

Map 中的 key:无序的、不可重复的,使用 Set 存储所有的 key --> key 所在的类要重写 equals() 和 hashCode()

Map 中的 value:有序的、可重复的,使用 Collection 存储所有的 value --> value 所在的类要重写 equals()

一个键值对:key-value 构成了一个 Entry 对象

Map 中的 Entry:无序的、不可重复的,使用 Set 存储所有的 entry

图示

常用方法

增:put(Object key,Object value)

删:remove(Object key)

改:put(Object key,Object value)

查:get(Object key)

长度:size()

遍历:

keySet()

values()

entrySet()

内存结构说明

HashMap 在 JDK7 中实现原理

HashMap map = new HashMap();

在实例化以后,底层创建了长度是16的一维数组 Entry[] table

…可能已经执行过多次…

map.put(key1, value1)

首先,调用 key1 所在类的 hashCode() 计算 key1 的哈希值,此哈希值经过某种算法计算以后,得到在 Entry 数组中存放的位置

如果此位置上的数据为空,此时的 key1-value1 添加成功 --> 情况1

如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较 key1 和已经存在的一个或多个数据的哈希值

如果 key1 的哈希值与已经存在的数据的哈希值都不相同,此时的 key1-value1 添加成功 --> 情况2

如果 key1 的哈希值与已经存在的某一个数据(key2-value2)的哈希值相同,继续比较:调用 key1 所在类的 equals(key2) 方法,比较

如果 equals() 返回 false:此时的 key1-value1 添加成功 --> 情况3

如果 equals() 返回 true:使用 value1 替换 value2

关于情况2和情况3:此时的 key1-value1 和原来的数据以链表的方式存储

在不断的添加过程中,会涉及到扩容问题,默认的扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来

HashMap 在 JDK8 相较于 JDK7 在底层实现方面的不同

new HashMap():底层没有创建一个长度为16的数组

JDK8 底层的数组是:Node[],而非 Entry[]

首次调用 put() 方法时,底层创建长度为16的数组

JDK7 底层结构只有:数组 + 链表,JDK8 中底层结构:数组 + 链表 + 红黑树

形成链表时,七上八下

JDK7:新的元素指向旧的元素

JDK8:旧的元素指向新的元素

当数组的某一个索引位置上的元素以链表形式存在的数据个数大于8且当前数组长度超过64时,此时此索引位置上的所有数据改为使用红黑树存储

HashMap底层典型属性的说明

DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16

DEFAULT_LOAD_FACTOR:HashMap的默认加载因子:0.75

threshold:扩容的临界值,= 容量 * 填充因子 16 * 0.75 = 12

TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树:8

MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量:64

LinkedHashMap 的底层实现原理

LinkedHashMap 底层使用的结构与 HashMap 相同,因为 LinkedHashMap 继承于 HashMap,区别就在于:LinkedHashMap 内部提供了 Entry,替换 HashMap 中的 Node

HashMap 中的 Node:

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }
    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }
    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }
    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }
    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

LinkedHashMap 中的 Entry

static class Entry<K,V> extends HashMap.Node<K,V> {
      Entry<K,V> before, after; // 能够记录添加元素的先后顺序
      Entry(int hash, K key, V value, Node<K,V> next) {
          super(hash, key, value, next);
      }
  }

TreeMap 的使用

向 TreeMap 中添加 key-value,要求 key 必须是有同一个类创建的对象,因为要按照 key 进行排序:自然排序 定制排序

使用 Properties 读取配置文件

常用于处理属性文件,key 和 value 都是字符串类型

public static void main(String[] args)  {
    FileInputStream fileInputStream = null;
    try {
        Properties prop = new Properties();
        fileInputStream = new FileInputStream("jdbc.properties");
        prop.load(fileInputStream);
        String name = prop.getProperty("name");
        String password = prop.getProperty("password");
        System.out.println("name = " + name);
        System.out.println("password = " + password);
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (fileInputStream != null) {
            try {
                fileInputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

Collections 工具类的使用

作用

操作 Collection 和 Map 的工具类

常用方法

reverse(List):反转 List 中元素的顺序

shuffle(List):对 List 集合元素进行随机排序

sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序

sort(List,Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序

swap(List,int, int):将指定 list 集合中的 i 处元素和 j 处元素进行交换

Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素

Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素

Object min(Collection):根据元素的自然顺序,返回给定集合中的最小元素

Object min(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最小元素

int frequency(Collection,Object):返回指定集合中指定元素的出现次数

void copy(List dest, List src):将 src 中的内容复制到 dest 中

boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所有旧值

说明:ArrayList 和 HashMap 都是线程不安全的,如果程序要求线程安全,可以将 ArrayList 和 HashMap转换为线程安全的,使用 synchronizedList(List list) 和 synchronizedMap(Map map)

面试题

Collection 和 Collections 的区别

数据结构简述

概述

数据结构(Data Structure)是一门和计算机硬件与软件都密切相关的学科,它的研究重点是在计算机的程序设计领域中探讨如何在计算机中组织和存储数据并进行高效率的运用,涉及的内容包含:数据的逻辑关系、数据的存储结构、排序算法(Algorithm)、查找(或搜索)等。

数据结构和算法的理解

**序能否快速而高效地完成预定的任务,取决于是否选对了数据结构,而程序是否能清楚而正确地把问题解决,则取决于算法。**算法是计算机处理信息的本质,因为计算机程序本质上是一个算法来告诉计算机确切的步骤来执行一个指定的任务。

所以大家认为:“Algorithms + Data Structures = Programs”(出自:Pascal之父Nicklaus Wirth)

总结:算法是为了解决实际问题而设计的,数据结构是算法需要处理的问题载体。

数据结构的研究对象

数据间的逻辑结构

数据的存储结构

线性表(顺序表、链表、栈、队列)

说明:

习惯上把顺序表和链表看做基本数据结构(或真实数据结构)

习惯上把栈、队列、树、图看成抽象数据类型,简称 ADT

泛型

泛型的理解

泛型的概念

所谓泛型,就是允许在定义类、接口时通过一个标识表示类中某个属性的类 型或者是某个方法的返回值及参数类型。这个类型参数将在使用时(例如, 继承或实现这个接口,用这个类型声明变量、创建对象时)确定(即传入实 际的类型参数,也称为类型实参)。

泛型的引入背景

集合容器类在设计阶段/声明阶段不能确定这个容器到底实际存的是什么类型的 对象,所以在JDK1.5之前只能把元素类型设计为Object,JDK1.5之后使用泛型来 解决。因为这个时候除了元素的类型不确定,其他的部分是确定的,例如关于 这个元素如何保存,如何管理等是确定的,因此此时把元素的类型设计成一个 参数,这个类型参数叫做泛型。Collection,List,ArrayList 这个就是类型参数,即泛型。

泛型在集合中的使用

集合中使用泛型之前的例子

@Test
public void test1(){
    ArrayList list = new ArrayList();
    list.add(78);
    list.add(77);
    list.add(89);
    list.add(88);
    // 问题一:类型不安全
    // list.add("Tom");
    for (Object score : list) {
        // 问题二:强转时,可能出现 ClassCastException
        int stuScore = (int) score;
        System.out.println(stuScore);
    }
}

图示

在集合中使用泛型例子1
@Test
public void test2(){
    // ArrayList<Integer> list = new ArrayList<Integer>();
    // JDK7 新特性:类型推断
    ArrayList<Integer> list = new ArrayList<>();
    list.add(78);
    list.add(87);
    list.add(99);
    list.add(65);
    // 编译时,就会进行类型检查,保证数据的安全
    // list.add("65");
    /*for(Integer score : list){
        // 避免了强转操作
        int stuScore = score;
        System.out.println(stuScore);
    }*/
    Iterator<Integer> iterator = list.iterator();
    while (iterator.hasNext()){
        int stuScore = iterator.next();
        System.out.println(stuScore);
    }
}

图示

在集合中使用泛型例子2
@Test
public void test3(){
    Map<String, Integer> map = new HashMap<>();
    map.put("Tom", 87);
    map.put("Jerry", 87);
    map.put("Jack", 67);
    // map.put(123, "67");
    Set<Map.Entry<String, Integer>> entry = map.entrySet();
    Iterator<Map.Entry<String, Integer>> iterator = entry.iterator();
    while (iterator.hasNext()){
        Map.Entry<String, Integer> e = iterator.next();
        String key = e.getKey();
        Integer value = e.getValue();
        System.out.println(key + " --- " + value);
    }
}

集合中使用泛型总结

集合接口或集合类在 JDK5.0 时都修改为带泛型的结构

在实例化集合类时,可以指明具体的泛型类型

指明完以后,在集合类或接口中凡是定义类或接口时,内部结构(方法,构造器,属性等)使用到类的泛型的位置,都指定为实例化的泛型类型。比如:add(E e) --> 实例化以后:add(Integer e)

注意点:泛型的类型必须是类,不能是基本数据类型,需要用到基本数据类型的位置,拿包装类替换

如果实例化时没有指明泛型的类型,默认类型为 java.lang.Object 类型

自定义泛型类、泛型接口、泛型方法

举例

[Order.java]

public class Order<T> {
    String orderName;
    int orderId;
    // 类的内部结构就可以使用类的泛型
    T orderT;
    public Order(){
        // 编译不通过
        // T[] arr = new T[10];
        // 编译通过
        T[] arr = (T[]) new Object[10];
    }
    public Order(String orderName, int orderId, T orderT) {
        this.orderName = orderName;
        this.orderId = orderId;
        this.orderT = orderT;
    }
    public String getOrderName() {
        return orderName;
    }
    public void setOrderName(String orderName) {
        this.orderName = orderName;
    }
    public int getOrderId() {
        return orderId;
    }
    public void setOrderId(int orderId) {
        this.orderId = orderId;
    }
    public T getOrderT() {
        return orderT;
    }
    public void setOrderT(T orderT) {
        this.orderT = orderT;
    }
    @Override
    public String toString() {
        return "Order{" +
                "orderName='" + orderName + '\'' +
                ", orderId=" + orderId +
                ", orderT=" + orderT +
                '}';
    }
    // 静态方法中不能使用类的泛型
    /*public static void show(){
        System.out.println(orderT);
    }*/
    public void show(){
        // 编译不通过
       /*try {
       }catch (T t){
       }*/
    }
    // 泛型方法:在方法中出现了泛型的结构,泛型参数与类的泛型参数没有任何关系
    // 换句话说,泛型方法所属的类是不是泛型类都没有关系
    // 泛型方法可以声明为静态的,原因:泛型参数是在调用方法时确定的,并非在实例化类时确定
    public static <E> List<E> copyFromArrayToList(E[] arr){
        ArrayList<E> list = new ArrayList<>();
        for (E e : arr) {
            list.add(e);
        }
        return list;
    }
}

[SubOrder.java]

public class SubOrder extends Order<Integer> {
    public static <E> List<E> copyFromArrayToList(E[] arr){
        ArrayList<E> list = new ArrayList<>();
        for (E e : arr) {
            list.add(e);
        }
        return list;
    }
}

[SubOrder1.java]

public class SubOrder1<T> extends Order<T>{
}

测试:

public class GenericTest1 {
    @Test
    public void test1(){
        // 如果定义了泛型类,实例化没有指明类的泛型,则认为此泛型类型为 Object 类型
        // 要求:如果定义了类是带泛型的,建议在实例化时要指明类的泛型
        Order order = new Order();
        order.setOrderT(123);
        order.setOrderT("123");
        // 建议:实例化时指明类的泛型
        Order<String> order1 = new Order<>("orderAA",
                1001, "order:AA") ;
        order1.setOrderT("AA:hello");
    }
    @Test
    public void test2(){
        // 由于子类在继承带泛型的父类时,指明了泛型类型,则实例化子类对象时,不再需要指明泛型
        SubOrder subOrder = new SubOrder();
        subOrder.setOrderT(1122);
        SubOrder1<String> subOrder1 = new SubOrder1<>();
        subOrder1.setOrderT("order2...");
    }
    @Test
    public void test3(){
        // 泛型不同的引用不能相互赋值
        ArrayList<String> list1 = null;
        ArrayList<Integer> list2 = new ArrayList<>();
        // list1 = list2;
        Person p1 = null;
        Person p2 = null;
        p1 = p2;
    }
    // 测试泛型方法
    @Test
    public void test4(){
        Order<String> order = new Order<>();
        Integer[] arr = {1, 2, 3, 4};
        // 泛型方法在调用时,指明泛型参数的类型
        List<Integer> list = order.copyFromArrayToList(arr);
        System.out.println(list);
    }
}

注意点

泛型类可能有多个参数,此时应将多个参数一起放在尖括号内。比如:

泛型类的构造器如下:public GenericClass(){}。 而下面是错误的:public GenericClass(){}

实例化后,操作原来泛型位置的结构必须与指定的泛型类型一致。

泛型不同的引用不能相互赋值。

尽管在编译时ArrayList和ArrayList是两种类型,但是,在运行时只有

一个ArrayList被加载到JVM中。

泛型如果不指定,将被擦除,泛型对应的类型均按照Object处理,但不等价 于Object。经验:泛型要使用一路都用。要不用,一路都不要用。

如果泛型结构是一个接口或抽象类,则不可创建泛型类的对象。

jdk1.7,泛型的简化操作:ArrayList flist = new ArrayList<>();

泛型的指定中不能使用基本数据类型,可以使用包装类替换。

在类/接口上声明的泛型,在本类或本接口中即代表某种类型,可以作为非静态 属性的类型、非静态方法的参数类型、非静态方法的返回值类型。但在静态方法 中不能使用类的泛型。

异常类不能是泛型的

不能使用new E[]。但是可以:E[] elements = (E[])new Object[capacity];

参考:ArrayList源码中声明:Object[] elementData,而非泛型参数类型数组。

父类有泛型,子类可以选择保留泛型也可以选择指定泛型类型:

子类不保留父类的泛型:按需实现

没有类型 擦除

具体类型

子类保留父类的泛型:泛型子类

全部保留

部分保留

结论:子类必须是“富二代”,子类除了指定或保留父类的泛型,还可以增加自己的泛型

应用场景举例

[DAO.java]:定义了操作数据库中的表的通用操作。ORM 思想(数据库中的表和 Java 中的类对应)

public class DAO<T> {
    // 添加一条记录
    public void add(T t){
    }
    // 删除一条记录
    public boolean remove(int index){
        return false;
    }
    // 修改一条记录
    public void update(int index, T t){
    }
    // 查询一条记录
    public T getIndex(int index){
        return null;
    }
    // 查询多条记录
    public List<T> getForList(int index){
        return null;
    }
    public <E> E getValue(){
        return null;
    }
}

[CustomerDAO.java]

public class CustomerDAO extends DAO<Customer>{
}

[StudentDAO.java]

public class StudentDAO extends DAO<Student>{
}

泛型在继承上的体现

虽然类 A 是类 B 的父类,但是 G 和 G 二者不具备子父类关系,二者是并列关系

补充:类 A 是类 B 的父类,A 是 B 的父类

@Test
public void test1(){
    Object obj = null;
    String str = null;
    obj = str;
    Date date = new Date();
    // 编译不通过
    // str = date;
    Object[] arr1 = null;
    String[] arr2 = null;
    arr1 = arr2;
    List<Object> list1 = null;
    List<String> list2 = new ArrayList<>();
    // 此时的 list1 和 list2 的类型不具备子父类关系
    // 编译不通过
    // list1 = list2;
    /*
        反证法
        假设 list1 = list2; // 导致混入非 String 的数据,出错
     */
    show(list1);
    // show(list2);
}
public void show(List<Object> list){
}
@Test
public void test2(){
    AbstractList<String> list1 = null;
    List<String> list2 = null;
    ArrayList<String> list3 = null;
    list1 = list3;
    list2 = list3;
}

通配符

通配符的使用

通配符:?

类 A 是类 B 的父类,G 和 G 是没有关系的,二者共同的父类是:G

@Test
public void test3() {
    List<Object> list1 = null;
    List<String> list2 = null;
    List<?> list = null;
    list = list1;
    list = list2;
    // print(list1);
    // print(list2);
    List<String> list3 = new ArrayList<>();
    list3.add("AA");
    list3.add("BB");
    list3.add("CC");
    list = list3;
    // 添加(写入):对于 List<?> 就不能向其内部添加数据,除了添加 null 之外
    // list.add("DD");
    list.add(null);
    // 获取(读取):允许读取数据,读取的数据类型为 Object
    Object o = list.get(0);
    System.out.println(o);
}
public void print(List<?> list){
    Iterator<?> iterator = list.iterator();
    while (iterator.hasNext()){
        Object obj = iterator.next();
        System.out.println(obj);
    }
}

涉及到通配符的集合的数据的写入和读取

@Test
public void test3() {
    List<Object> list1 = null;
    List<String> list3 = new ArrayList<>();
    list3.add("AA");
    list3.add("BB");
    list3.add("CC");
    list = list3;
    // 添加(写入):对于 List<?> 就不能向其内部添加数据,除了添加 null 之外
    // list.add("DD");
    list.add(null);
    // 获取(读取):允许读取数据,读取的数据类型为 Object
    Object o = list.get(0);
    System.out.println(o);
}

有限制条件的通配符的使用

? extends A:G 可以作为 G 和 G 的父类,其中 B 是 A 的子类

@Test
public void test4(){
    List<? extends Person> list1 = null;
    List<? super Person> list2 = null;
    List<Student> list3 = new ArrayList<>();
    List<Person> list4 = new ArrayList<>();
    List<Object> list5 = new ArrayList<>();
    list1 = list3;
    list1 = list4;
    // list1 = list5;
    // list2 = list3;
    list2 = list4;
    list2 = list5;
    // 读取数据
    list1 = list3;
    Person person = list1.get(0);
    // 编译不通过
    // Person person1 = list1.get(0);
    list2 = list4;
    Object obj = list2.get(0);
    // 写入数据:
    // list1.add(new Student());
    list2.add(new Person());
    list2.add(new Student());
}

IO 流

File 类的使用

File 类的理解

File 类的一个对象,代表一个文件或一个目文件录

File 类声明在 java.io 包下

File 类中涉及到关于文件或文件目录的创建、删除、重命名、修改时间、文件大小等方法,并未涉及到写入或读取文件内容的操作,如果需要读取或写入文件内容,必须使用 IO 流来完成

后续 File 类的对象常会作为参数传递到流的构造器中,指明读取或写入的“终点”

File 的实例化

常用构造器

File(String filepath)

File(String parentPath, String childPath)

File(File parentFile, String childPath)

路径的分类

相对路径:相较于某个路径下,指明的路径

绝对路径:包含盘符在内的文件或文件目录的路径

说明:

IDEA

如果开发使用 JUnit 中的 单元测试方法测试,相对路径即为当前 module 下

如果开发使用 main() 方法测试,相对路径即为当前 Project 下

Eclipse

不管使用单元测试方法还是使用 main() 方法测试,相对路径都是为当前 Project 下

路径分隔符

windows 和 DOS 系统默认使用“\”来表示

UNIX 和 URL 使用“/”来表示

File 类的常用方法

File类的获取功能

public String getAbsolutePath():获取绝对路径

public String getPath():获取路径

public String getName():获取名称

public String getParent():获取上层文件目录路径。若无,返回 null

public long length():获取文件长度(即:字节数)。不能获取目录的长度。

public long lastModified():获取最后一次的修改时间,毫秒值

如下的两个方法适用于文件目录

public String[] list():获取指定目录下的所有文件或者文件目录的名称数组

public File[] listFiles():获取指定目录下的所有文件或者文件目录的 File 数组

File类的重命名功能

public boolean renameTo(File dest):把文件重命名为指定的文件路径

File类的判断功能

public boolean isDirectory():判断是否是文件目录

public boolean isFile():判断是否是文件

public boolean exists():判断是否存在

public boolean canRead():判断是否可读

public boolean canWrite():判断是否可写

public boolean isHidden():判断是否隐藏

File 类的创建功能

public boolean createNewFile():创建文件。若文件存在,则不创建,返回false

public boolean mkdir():创建文件目录。如果此文件目录存在,就不创建了。 如果此文件目录的上层目录不存在,也不创建。

public boolean mkdirs():创建文件目录。如果上层文件目录不存在,一并创建

注意事项:如果你创建文件或者文件目录没有写盘符路径,那么,默认在项目 路径下。

File 类的删除功能

public boolean delete():删除文件或者文件夹

删除注意事项:Java中的删除不走回收站。 要删除一个文件目录,请注意该文件目录内不能包含文件或者文件目录

IO流概述

流的分类

操作数据单位

字节流

字符流

数据的流向

输入流

输出流

流的角色

节点流

处理流

图示:

流的体系结构

image.png

重点说明的几个流结构

image.png输入输出的标准化过程

输入过程

创建 File 类的对象,指明数据的来源(要求此文件一定存在)

创建相应的输入流,将 File 类的对象作为参数,传入流的构造器中

具体的读入过程:创建相应的 byte[] 或 char[]

关闭流资源

说明:程序中出现的异常需要使用 try-catch-finally 处理

输出过程

创建 File 类的对象,指明写出数据的位置(不要求此文件一定存在)

创建相应的输出流,将 File 类的对象作为参数,传入流的构造器中

具体的写出过程:write(char[] / byte[] buffer, 0, len)

关闭流资源

说明:程序中出现的异常需要使用 try-catch-finally 处理

节点流(或文件流)

FileReader / FileWrite 的使用

FileReader 的使用

说明点:

read() 的理解:返回读入的一个字符,如果达到文件末尾,返回-1

异常的处理:为了保证流资源一定可以执行关闭操作,需要使用 try-catch-finally 处理

读入的文件一定要存在,否则就会报 FileNotFoundException


@Test
public void testFileReader1() {
    FileReader fileReader = null;
    try {
        // 1.File 类的实例化
        File file = new File("hello.txt");
        // 2.FileReader 流的实例化
        fileReader = new FileReader(file);
        // 3.读入的操作
        // read(char[] cbuf):返回每次读入 cbuf 数组中的字符的个数,如果达到文件末尾,返回-1
        char[] cbuf = new char[5];
        int len = 0;
        while ((len = fileReader.read(cbuf)) != -1){
            // 错误的写法
            /*for (int i = 0; i < cbuf.length; i++) {
                System.out.print(cbuf[i]);
            }*/
            // 正确的写法
            /*for (int i = 0; i < len; i++) {
                System.out.print(cbuf[i]);
            }*/
            // 错误的写法
            // System.out.print(new String(cbuf));
            // 正确的写法
            System.out.print(new String(cbuf, 0, len));
        }
    } catch (IOException e){
        e.printStackTrace();
    } finally{
        // 4.资源的关闭
        if (fileReader != null) {
            try {
                fileReader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

FileWriter 的使用

说明:

输出操作,对应的 File 可以不存在,并不会报异常

File 对应的硬盘中的文件如果不存在,在输出的过程中,会自动创建此文件

File 对应的硬盘中的文件如果存在:

如果流使用的构造器是:FileWriter(file, false) / FileWriter(file):对原有文件覆盖

如果流使用的构造器是:FileWriter(file, true):不会对原有文件覆盖,而是在原有文件基础上追加内容

@Test
public void testFileWriter(){
    FileWriter fileWriter = null;
    try {
        // 1.提供 File 类的对象,指明写出到的文件
        File file = new File("hello1.txt");
        // 2.提供 FileWriter 的对象,用于数据的写出
        fileWriter = new FileWriter(file, false);
        // 3.写出的操作
        fileWriter.write("I have a dream!\n");
        fileWriter.write("you need to have a dream!");
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        // 4.流资源的关闭
        if (fileWriter != null) {
            try {
                fileWriter.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

文本文件的复制

@Test
public void testFileReaderFileWriter() {
    FileReader fileReader = null;
    FileWriter fileWriter = null;
    try {
        // 1.创建 File 类的对象,指明读入和写出的文件
        // File srcFile = new File("hello.txt");
        // File destFile = new File("hello2.txt");
        // 不能使用字符流来处理图片等字节数据
        File srcFile = new File("爱情与友情.png");
        File destFile = new File("爱情与友情1.png");
        // 2.创建输入流和输出流的对象
        fileReader = new FileReader(srcFile);
        fileWriter = new FileWriter(destFile);
        // 3.数据的读入和写出操作
        char[] cbuf = new char[5];
        // 记录每次读入到 cbuf 数组中的字符的个数
        int len = 0;
        while ((len = fileReader.read(cbuf)) != -1){
            // 每次写出 len 个字符
            fileWriter.write(cbuf, 0, len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        // 4.关闭流资源
        if (fileWriter != null) {
            try {
                fileWriter.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (fileReader != null) {
            try {
                fileReader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

FileInputStream / FileOutputStream 的使用

对于文本文件(.txt, .java, .c, .cpp),使用字符流处理

对于非文本文件(.jpg, .mp3, .mp4, .avi, .doc, .ppt, …),使用字节流处理

/**
 * 实现对图片的复制
 */
@Test
public void testFileInputOutputStream(){
    FileInputStream fileInputStream = null;
    FileOutputStream fileOutputStream = null;
    try {
        File srcFile = new File("爱情与友情.png");
        File destFile = new File("爱情与友情2.png");
        fileInputStream = new FileInputStream(srcFile);
        fileOutputStream = new FileOutputStream(destFile);
        // 复制的过程
        byte[] buffer = new byte[5];
        int len = 0;
        while ((len = fileInputStream.read(buffer)) != -1){
            fileOutputStream.write(buffer, 0, len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }finally {
        if (fileInputStream != null) {
            try {
                fileInputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (fileOutputStream != null) {
            try {
                fileOutputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

注意点:相对路径在 IDEA 和 Eclipse 中使用的区别?

IDEA

如果使用单元测试方法,相对路径基于当前 Module 的

如果使用 main(),相对路径基于当前 Project 的

Eclipse:不管是单元测试方法还是 main(),相对路径都是基于当前 Project 的

缓冲流的使用

缓冲流涉及到的类

BufferedInputStream

BufferedOutputStream

BufferedReader

BufferedWriter

作用

作用:提高流的读取、写入的速度

提高读写速度的原因:内部提供了一个缓冲区,默认情况下是8kb

public class BufferedInputStream extends FilterInputStream {
    private static int DEFAULT_BUFFER_SIZE = 8192;
}

典型代码

使用 BufferedInputStream 和 BufferedOutputStream

处理非文本文件

/**
 * 实现文件复制的方法
 */
public void copyFileWithBuffered(String srcPath, String destPath){
    BufferedInputStream bufferedInputStream = null;
    BufferedOutputStream bufferedOutputStream = null;
    try {
        // 造文件
        File srcFile = new File(srcPath);
        File destFile = new File(destPath);
        // 2.造流
        // 2.1.造节点流
        FileInputStream fileInputStream = new FileInputStream(srcFile);
        FileOutputStream fileOutputStream = new FileOutputStream(destFile);
        // 2.2.造缓冲流
        bufferedInputStream = new BufferedInputStream(fileInputStream);
        bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
        // 3.复制的细节
        byte[] buffer = new byte[1024];
        int len = 0;
        while ((len = bufferedInputStream.read(buffer)) != -1){
            bufferedOutputStream.write(buffer, 0, len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        // 4.资源关闭
        // 要求:先关闭外层的流,再关闭内层的流
        if (bufferedOutputStream != null) {
            try {
                bufferedOutputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (bufferedInputStream != null) {
            try {
                bufferedInputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        // 说明:在关闭外层流的同时,内层流也会自动进行关闭,对于内层流的关闭,可以省略
        // fileOutputStream.close();
        // fileInputStream.close();
    }
}

使用 BufferedReader 和 BufferedWriter

处理文本文件

/**
 * 使用 BufferedReader 和 BufferedWriter 实现文本文件的复制
 */
@Test
public void testBufferedReaderBufferedWriter(){
    BufferedReader bufferedReader = null;
    BufferedWriter bufferedWriter = null;
    try {
        // 创建文件和相应的流
        bufferedReader = new BufferedReader(new FileReader(new File("dbcp.txt")));
        bufferedWriter = new BufferedWriter(new FileWriter(new File("dbcp1.txt")));
        // 读写操作
        // 方式一,使用 char[] 数组
        /*char[] cbuf = new char[1024];
        int len = 0;
        while ((len = bufferedReader.read(cbuf)) != -1){
            bufferedWriter.write(cbuf, 0, len);
        }*/
        // 方式二:使用 String
        String data;
        while ((data = bufferedReader.readLine()) != null){
            // 方法一:
            // data 中不包含换行符
            // bufferedWriter.write(data + "\n");
            // 方法二:
            bufferedWriter.write(data);
            // 提供换行的操作
            bufferedWriter.newLine();
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (bufferedReader != null) {
            try {
                bufferedReader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (bufferedWriter != null) {
            try {
                bufferedWriter.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

转换流的使用

转换流涉及到的类

属于字符流


InputStreamReader:将一个字节的输入流转换为字符的输入流


解码:字节、字节数组 --> 字符数组、字符串


OutputStreamWriter:将一个字符的输出流转换为字节的输出流


编码:字符数组、字符串 --> 字节、字节数组


说明:编码决定了解码的方式


作用

提供字节流与字符流之间的转换


图示

典型实现

@Test
public void test1(){
    InputStreamReader inputStreamReader = null;
    try {
        FileInputStream fileInputStream = new FileInputStream("dbcp.txt");
        // 使用系统默认的字符集
        // InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream);
        // 参数2指明了字符集:具体使用哪个字符集,取决于文件 dbcp.txt 保存时使用的字符集
        inputStreamReader = new InputStreamReader(fileInputStream, StandardCharsets.UTF_8);
        char[] cbuf = new char[20];
        int len = 0;
        while ((len = inputStreamReader.read(cbuf)) != -1){
            System.out.print(new String(cbuf,0, len));
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (inputStreamReader != null) {
            try {
                inputStreamReader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
/**
 * 综合使用 InputStreamReader 和 OutputStreamWriter
 */
@Test
public void test2(){
    InputStreamReader inputStreamReader = null;
    OutputStreamWriter outputStreamWriter = null;
    try {
        File file1 = new File("dbcp.txt");
        File file2 = new File("dbcp_gbk.txt");
        FileInputStream fileInputStream = new FileInputStream(file1);
        FileOutputStream fileOutputStream = new FileOutputStream(file2);
        inputStreamReader = new InputStreamReader(fileInputStream, StandardCharsets.UTF_8);
        outputStreamWriter = new OutputStreamWriter(fileOutputStream, "gbk");
        char[] cbuf = new char[20];
        int len = 0;
        while ((len = inputStreamReader.read(cbuf)) != -1){
            outputStreamWriter.write(cbuf, 0, len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }finally {
        if (inputStreamReader != null) {
            try {
                inputStreamReader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (outputStreamWriter != null) {
            try {
                outputStreamWriter.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

说明

文件编码的方式,决定了解析时使用的字符集

编码表

常见编码表

ASCII:美国标准信息交换码,用一个字节的7位可以表示。

ISO8859-1:拉丁码表。欧洲码表,用一个字节的8位表示。

GB2312:中国的中文编码表。最多两个字节编码所有字符

GBK:中国的中文编码表升级,融合了更多的中文文字符号。最多两个字节编码

Unicode:国际标准码,融合了目前人类使用的所有字符。为每个字符分配唯一的字符码。所有的文字都用两个字节来表示。

UTF-8:变长的编码方式,可用1-4个字节来表示一个字符。

启示

客户端 / 游览器端 <—> 后台(Java, Go, Python, Node.js, PHP) <—> 数据库

要求前前后后使用的字符集都要统一:UTF-8

其它的流的使用

标准的输入输出流

System.in:标准的输入流,默认从键盘输入

System.out:标准的输出流,默认从控制台输出

修改默认的输入和输出行为:System 类的 setIn(InputStream InputStream) / setOut(OutputStream outputStream) 方式重新指定输入和输出的流

打印流

PrintStream

PrintWriter

说明:

提供了一系列重载的 print() 和 println() 方法,用于多种数据类型的输出

System.out 返回的是 PrintStream 的实例

数据流

DataInputStream

DataOutputStream

作用:用于读取或写出基本数据类型的变量或字符串

示例代码:

/**
 * 数据流:DataInputStream 和 DataOutputStream
 * 1.作用:用于读取或写出基本数据类型的变量或字符串
 */
@Test
public void test3(){
    DataOutputStream dataOutputStream = null;
    try {
        dataOutputStream = new DataOutputStream(new FileOutputStream("data.txt"));
        dataOutputStream.writeUTF("刘建辰");
        dataOutputStream.writeInt(23);
        dataOutputStream.writeBoolean(true);
        // 刷新操作,将内存中的数据写入文件
        dataOutputStream.flush();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (dataOutputStream != null) {
            try {
                dataOutputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
/**
 * 将文件中存储的基本数据类型变量和字符串读取到内存中
 * 注意点:读取不同类型数据的顺序要以当初写入文件时,保存的数据的顺序一致
 */
@Test
public void test4(){
    DataInputStream dataInputStream = null;
    try {
        dataInputStream = new DataInputStream(new FileInputStream("data.txt"));
        System.out.println(dataInputStream.readUTF());
        System.out.println(dataInputStream.readInt());
        System.out.println(dataInputStream.readBoolean());
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (dataInputStream != null) {
            try {
                dataInputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

对象流的使用

对象流

ObjectInputStream

ObjectOutputStream

作用

ObjectOutputStream:内存中的对象 --> 存储中的文件、通过网络传输出去:序列化过程

ObjectInputStream:存储中的文件、通过网络接收过来 --> 内存中的对象:反序列化过程

对象的序列化机制

对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。当其它程序获取了这种二进制流,就可以恢复成原来的 Java 对象

序列化过程

@Test
public void testObjectOuputStream(){
    ObjectOutputStream objectOutputStream = null;
    try {
        objectOutputStream = new ObjectOutputStream(new FileOutputStream("object.dat"));
        objectOutputStream.writeObject(new String("我爱北京天安门"));
        // 刷新操作
        objectOutputStream.flush();
        objectOutputStream.writeObject(new Person("王铭", 23));
        objectOutputStream.flush();
        objectOutputStream.writeObject(new Person("张学良", 23, 1001, new Account(5000)));
        objectOutputStream.flush();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (objectOutputStream != null) {
            try {
                objectOutputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

反序列化过程

@Test
public void testObjectInputStream(){
    ObjectInputStream objectInputStream = null;
    try {
        objectInputStream = new ObjectInputStream(new FileInputStream("object.dat"));
        Object obj = objectInputStream.readObject();
        String str = (String) obj;
        System.out.println(str);
        Person person = (Person) objectInputStream.readObject();
        System.out.println(person);
        Person person1 = (Person) objectInputStream.readObject();
        System.out.println(person1);
    } catch (IOException | ClassNotFoundException e) {
        e.printStackTrace();
    } finally {
        if (objectInputStream != null) {
            try {
                objectInputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

实现序列化的对象所属的类需要满足的条件

需要实现接口:Serializable

需要当前类提供一个全局常量:serialVersionUID

除了当前类需要实现 Serializable 接口之外,还必须保证其内部所有实现也必须是可序列化的(默认情况下,基本数据类型可序列化)

补充:ObjectOutputStream 和 ObjectInputStream 不能序列化 static 和 transient 修饰的成员变量

RandomAccessFile 的使用

随机存取文件流

RandomAccessFile

使用说明

RandomAccessFile 直接继承于 java.lang.Object 类的,实现了 DataInput 和 DataOutput 接口

RandomAccessFile 即可以作为一个输入流,又可以作为一个输出流

如果 RandomAccessFile 作为输出流时,写出到的文件如果不存在,则在执行过程中自动创建,写出到的文件如果存在,则会对原有文件内容进行覆盖(默认情况下,从头覆盖)

可以通过相关的操作,实现 RandomAccessFile “插入”数据的效果:seek(int pos)

典型代码

@Test
public void test1(){
    RandomAccessFile randomAccessFile1 = null;
    RandomAccessFile randomAccessFile2 = null;
    try {
        randomAccessFile1 = new RandomAccessFile(new File("爱情与友情.png"), "r");
        randomAccessFile2 = new RandomAccessFile(new File("爱情与友情1.png"), "rw");
        byte[] buffer = new byte[1024];
        int len = 0;
        while ((len = randomAccessFile1.read(buffer)) != -1){
            randomAccessFile2.write(buffer, 0, len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (randomAccessFile1 != null) {
            try {
                randomAccessFile1.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (randomAccessFile2 != null) {
            try {
                randomAccessFile2.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
@Test
public void test3() {
    RandomAccessFile randomAccessFile = null;
    ByteArrayOutputStream byteArrayOutputStream = null;
    try {
        randomAccessFile = new RandomAccessFile("hello.txt", "rw");
        // 将指针调到角标为3的位置
        randomAccessFile.seek(3);
        // 保存指针3后面的所有数据到 ByteArrayOutputStream / StringBuilder 中
        byteArrayOutputStream = new ByteArrayOutputStream();
        /*StringBuilder builder = new StringBuilder(
                (int) new File("hello.txt").length());*/
        byte[] buffer = new byte[20];
        int len = 0;
        while ((len = randomAccessFile.read(buffer)) != -1){
            // builder.append(new String(buffer, 0, len));
            byteArrayOutputStream.write(buffer, 0, len);
        }
        randomAccessFile.seek(3);
        randomAccessFile.write("xyz".getBytes());
        // randomAccessFile.write(builder.toString().getBytes());
        randomAccessFile.write(byteArrayOutputStream.toByteArray());
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (byteArrayOutputStream != null) {
            try {
                byteArrayOutputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (randomAccessFile != null) {
            try {
                randomAccessFile.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

Path、Paths、Files 的使用

NIO 的使用说明

Java NIO (New IO,Non-Blocking IO)是从Java 1.4版本开始引入的一套新 的IO API,可以替代标准的Java IO API。

lNIO与原来的IO有同样的作用和目 的,但是使用的方式完全不同,NIO支持面向缓冲区的(IO是面向流的)、基于通道的IO操作。

NIO将以更加高效的方式进行文件的读写操作。

随着 JDK 7 的发布,Java 对 NIO 进行了极大的扩展,增强了对文件处理和文件系统特性的支持,以至于我们称他们为 NIO.2。

Path 的使用 (JDK7 提供)

Path 的说明

Path 替换原有的 File 类

如何实例化

Paths 类提供的静态 get() 方法用来获取 Path 对象:

static Path get(String first, String … more): 用于将多个字符串串连成路径

static Path get(URI uri): 返回指定 uri 对应的 Path 路径

常用方法

String toString(): 返回调用 Path 对象的字符串表示形式

boolean startsWith(String path): 判断是否以 path 路径开始

boolean endsWith(String path): 判断是否以 path 路径结束

boolean isAbsolute(): 判断是否是绝对路径

Path getParent(): 返回Path对象包含整个路径,不包含 Path 对象指定的文件路径

Path getRoot(): 返回调用 Path 对象的根路径

Path getFileName(): 返回与调用 Path 对象关联的文件名

int getNameCount(): 返回 Path 根目录后面元素的数量

Path getName(int idx): 返回指定索引位置 idx 的路径名称

Path toAbsolutePath(): 作为绝对路径返回调用 Path 对象

Path resolve(Path p): 合并两个路径,返回合并后的路径对应的 Path 对象

File toFile(): 将 Path 转化为 File 类的对象

Files 工具类 (JDK7 提供)

作用

操作文件或文件目录的工具类

常用方法

Files常用方法

Path copy(Path src, Path dest, CopyOption … how): 文件的复制

Path createDirectory(Path path, FileAttribute … attr): 创建一个目录

Path createFile(Path path, FileAttribute … arr): 创建一个文件

void delete(Path path): 删除一个文件/目录,如果不存在,执行报错

void deleteIfExists(Path path): Path对应的文件/目录如果存在,执行删除

Path move(Path src, Path dest, CopyOption…how): 将 src 移动到 dest 位置

long size(Path path): 返回 path 指定文件的大小

用于判断

boolean exists(Path path, LinkOption … opts): 判断文件是否存在

boolean isDirectory(Path path, LinkOption … opts): 判断是否是目录

boolean isRegularFile(Path path, LinkOption … opts): 判断是否是文件

boolean isHidden(Path path): 判断是否是隐藏文件

boolean isReadable(Path path): 判断文件是否可读

boolean isWritable(Path path): 判断文件是否可写

boolean notExists(Path path, LinkOption … opts): 判断文件是否不存在

用于操作内容

SeekableByteChannel newByteChannel(Path path, OpenOption…how): 获取与指定文件的连接,how 指定打开方式。

DirectoryStream newDirectoryStream(Path path): 打开 path 指定的目录

InputStream newInputStream(Path path, OpenOption…how):获取 InputStream 对象

OutputStream newOutputStream(Path path, OpenOption…how): 获取 OutputStream 对象

网络编程

InetAddress 类的使用

实现网络通信需要解决的两个问题

如何准确地定位网络上一台或多台主机;定位主机上的特定的应用

找到主机后如何可靠高效地进行数据传输

网络通信的两个要素

对应问题一:IP 和端口号

对应问题二:网络通信协议:TCP/IP 参考模型(应用层、传输层、网络层、物理 + 数据链路层)

通信要素一:IP 和端口号

IP 的理解

IP:唯一的标识 Internet 上的计算机(通信实体)

在 Java 值使用 InetAddress 类代表 IP

IP 分类:IPv4 和 IPv6 / 万维网 和 局域网

域名:www.baidu.com www.mi.com www.sina.com www.vip.com

域名解析:域名容易记忆,当在连接网络时输入一个主机的域名后,域名服务器(DNS)负责将域名转化成IP地址,这样才能和主机建立连接。

本地回路地址:127.0.0.1 对应着 localhost

InetAddress 类

此类的一个对象表着一个 IP 地址

实例化:

getByName(String host)

getLocalhost()

常用方法:

getHostName()

getHostAddress()

端口号

端口号:正在计算机上运行的进程

要求:不同的进程有不同的端口号

范围:被规定为一个 16 位的整数 0~65535。

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

通信要素二:网络通信协议

分型模型

TCP 和 UDP 的区别

TCP协议:


使用TCP协议前,须先建立TCP连接,形成传输数据通道

传输前,采用“三次握手”方式,点对点通信,是可靠的

TCP协议进行通信的两个应用进程:客户端、服务端。

在连接中可进行大数据量的传输

传输完毕,需释放已建立的连接,效率低

UDP 协议:


将数据、源、目的封装成数据包,不需要建立连接

每个数据报的大小限制在64K内

发送不管对方是否准备好,接收方收到也不确认,故是不可靠的

可以广播发送

发送数据结束时无需释放资源,开销小,速度快

TCP 三次握手和四次挥手

TCP 网络编程

例子1:客户端发送信息给服务端,服务端将数据显示在控制台上

public class TCPTest1 {
    /**
     * 客户端
     */
    @Test
    public void client(){
        Socket socket = null;
        OutputStream outputStream = null;
        try {
            // 1.创建 Socket 对象,指明服务器的 IP 和端口号
            InetAddress inet = InetAddress.getByName("127.0.0.1");
            socket = new Socket(inet, 8899);
            // 2.获取一个输出流,用于输出数据
            outputStream = socket.getOutputStream();
            // 3.写出数据的操作
            outputStream.write("你好,我是客户端mm".getBytes());
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            // 4.资源的关闭
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    /**
     * 服务端
     */
    @Test
    public void server(){
        ServerSocket serverSocket = null;
        Socket socket = null;
        InputStream inputStream = null;
        ByteArrayOutputStream byteArrayOutputStream = null;
        try {
            // 1.创建服务器端的 ServerSocket,指明自己的端口号
            serverSocket = new ServerSocket(8899);
            // 2.调用 accept() 表示接收来自于客户端的 socket
            socket = serverSocket.accept();
            // 3.获取输入流
            inputStream = socket.getInputStream();
            // 不建议,有乱码
            /*byte[] buffer = new byte[1024];
            int len = 0;
            while ((len = inputStream.read(buffer)) != -1){
                System.out.print(new String(buffer, 0, len));
            }*/
            // 4.获取输入流中的数据
            byteArrayOutputStream = new ByteArrayOutputStream();
            byte[] buffer = new byte[5];
            int len = 0;
            while ((len = inputStream.read(buffer)) != -1){
                byteArrayOutputStream.write(buffer, 0, len);
            }
            System.out.println(byteArrayOutputStream);
            System.out.println("收到了来自于:" + socket.getInetAddress().getHostAddress() + " 的数据");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 5.资源的关闭
            if (byteArrayOutputStream != null) {
                try {
                    byteArrayOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (serverSocket != null) {
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

例题2:客户端发送文件给服务端,服务端将文件保存在本地

public class TCPTest2 {
    @Test
    public void client(){
        Socket socket = null;
        OutputStream outputStream = null;
        FileInputStream fileInputStream = null;
        try {
            socket = new Socket(InetAddress.getByName("127.0.0.1"), 9090);
            outputStream = socket.getOutputStream();
            fileInputStream = new FileInputStream("beauty.png");
            byte[] buffer = new byte[1024];
            int len = 0;
            while ((len = fileInputStream.read(buffer)) != -1){
                outputStream.write(buffer, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fileInputStream != null) {
                try {
                    fileInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    @Test
    public void server(){
        ServerSocket serverSocket = null;
        Socket socket = null;
        InputStream inputStream = null;
        FileOutputStream fileOutputStream = null;
        try {
            serverSocket = new ServerSocket(9090);
            socket = serverSocket.accept();
            inputStream = socket.getInputStream();
            fileOutputStream = new FileOutputStream("beauty1.png");
            byte[] buffer = new byte[1024];
            int len = 0;
            while ((len = inputStream.read(buffer)) != -1){
                fileOutputStream.write(buffer, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fileOutputStream != null) {
                try {
                    fileOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (serverSocket != null) {
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

例子3:从客户端发送文件给服务端,服务端保存到本地。并返回“发送成功”给客户端。并关闭相应的连接。

public class TCPTest3 {
    @Test
    public void client(){
        Socket socket = null;
        OutputStream outputStream = null;
        InputStream inputStream = null;
        FileInputStream fileInputStream = null;
        ByteArrayOutputStream byteArrayOutputStream = null;
        try {
            socket = new Socket(InetAddress.getByName("127.0.0.1"), 9090);
            outputStream = socket.getOutputStream();
            fileInputStream = new FileInputStream("beauty.png");
            byte[] buffer = new byte[1024];
            int len = 0;
            while ((len = fileInputStream.read(buffer)) != -1){
                outputStream.write(buffer, 0, len);
            }
            // 关闭数据的输出
            socket.shutdownOutput();
            // 接收来自于服务器端的数据,并显示到控制台
            inputStream = socket.getInputStream();
            byteArrayOutputStream = new ByteArrayOutputStream();
            byte[] bufferr = new byte[20];
            int len1 = 0;
            while ((len1 = inputStream.read(bufferr)) != -1){
                byteArrayOutputStream.write(bufferr, 0, len1);
            }
            System.out.println(byteArrayOutputStream);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (byteArrayOutputStream != null) {
                try {
                    byteArrayOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (fileInputStream != null) {
                try {
                    fileInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    @Test
    public void server(){
        ServerSocket serverSocket = null;
        Socket socket = null;
        InputStream inputStream = null;
        FileOutputStream fileOutputStream = null;
        OutputStream outputStream = null;
        try {
            serverSocket = new ServerSocket(9090);
            socket = serverSocket.accept();
            inputStream = socket.getInputStream();
            fileOutputStream = new FileOutputStream("beauty2.png");
            byte[] buffer = new byte[1024];
            int len = 0;
            while ((len = inputStream.read(buffer)) != -1){
                fileOutputStream.write(buffer, 0, len);
            }
            System.out.println("图片传输完成");
            // 服务器给予客户端反馈
            outputStream = socket.getOutputStream();
            outputStream.write("你好,美女,照片我已收到,非常漂亮".getBytes());
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (fileOutputStream != null) {
                try {
                    fileOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (serverSocket != null) {
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

UDP 网络编程

public class UDPTest {
    /**
     * 发送端
     */
    @Test
    public void sender(){
        DatagramSocket socket = null;
         try {
            socket = new DatagramSocket();
            String str = "我是 UDP 方式发送的导弹";
            byte[] data = str.getBytes();
            InetAddress inet = InetAddress.getLocalHost();
            DatagramPacket packet = new DatagramPacket(data, 0, data.length, inet, 9090);
            socket.send(packet);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            socket.close();
        }
    }
    /**
     * 接收端
     */
    @Test
    public void receiver(){
        DatagramSocket socket = null;
        try {
            socket = new DatagramSocket(9090);
            byte[] buffer = new byte[100];
            DatagramPacket packet = new DatagramPacket(buffer, 0, buffer.length);
            socket.receive(packet);
            System.out.println(new String(packet.getData(), 0, packet.getLength()));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            socket.close();
        }
    }
}

URL 编程

URL 的理解

Uniform Resource Locator:统一资源定位符,对应着互联网的某一资源地址

URL 的5个基本结构

http://localhost:8080/examples/beauty.png?p=629&spm_id_from=pageDriver

协议 主机名 端口号 资源地址 参数列表

实例化

URL url = new URL(“http://localhost:8080/examples/beauty.png?p=629&spm_id_from=pageDriver”);

常用方法

public String getProtocol():获取该 URL 的协议名

public String getHost():获取该 URL 的主机名

public String getPort():获取该 URL 的端口号

public String getPath():获取该 URL 的文件路径

public String getFile():获取该 URL 的文件名

public String getQuery():获取该 URL 的查询名

可以读取、下载对应的 url 资源

public class URLTest1 {
    public static void main(String[] args){
        HttpURLConnection urlConnection = null;
        InputStream inputStream = null;
        FileOutputStream fileOutputStream = null;
        try {
            URL url = new URL("http://localhost:8080/examples/beauty.png");
            urlConnection = (HttpURLConnection) url.openConnection();
            urlConnection.connect();
            inputStream = urlConnection.getInputStream();
            fileOutputStream = new FileOutputStream("JavaSenior\\day10\\beauty3.png");
            byte[] buffer = new byte[1024];
            int len = 0;
            while ((len = inputStream.read(buffer)) != -1){
                fileOutputStream.write(buffer, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fileOutputStream != null) {
                try {
                    fileOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
        }
    }
}

Java 反射机制

反射的概述

主要内容

Java 反射机制概述

理解 Class 类并获取 Class 实例

类的加载与 ClassLoader 的理解

创建运行时类的对象

获取运行时类的完整结构

调用运行时类的指定结构

反射的应用:动态代理

关于反射的理解

Reflection(反射)是被视为动态语言的关键,反射机制允许程序在执行期 借助于Reflection API取得任何类的内部信息,并能直接操作任意对象的内 部属性及方法。

框架 = 反射 + 注解 + 设计模式

体现反射机制的“动态性”

@Test
public void test2(){
    for (int i = 0; i < 100; i++) {
        int num = new Random().nextInt(3);
        String classPath = "";
        switch (num){
            case 0:
                classPath = "java.util.Date";
                break;
            case 1:
                classPath = "java.lang.Object";
                break;
            case 2:
                classPath = "cn.tedu.java.Person";
                break;
        }
        System.out.println(getInstance(classPath));
    }
}
/**
 * 创建一个指定类的对象
 * @param classPath 指定类的全类名
 * @return
 * @throws Exception
 */
public Object getInstance(String classPath){
    try {
        return Class.forName(classPath).newInstance();
    } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
        e.printStackTrace();
    }
    return null;
}

反射机制能提供的功能

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

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

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

在运行时获取泛型信息

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

在运行时处理注解

生成动态代理

相关 API

java.lang.Class:反射的源头

java.lang.reflect.Method

java.lang.reflect.Field

java.lang.reflect.Constructor

……

Class 类的理解与获取 Class 的实例

Class 类的理解

类的加载过程:

程序经过 javac.exe 命令以后,会生成一个或多个字节码文件(.class 结尾)。

接着使用 java.exe 命令对某个字节码文件进行解释运行,相当于将某个字节码文件加载到内存中,此过程称为类的加载

加载到内存中的类,就称为运行时类,此运行时类,就作为 Class 的一个实例

换句话说,Class 的实例就对应着一个运行时类

加载到内存中的运行时类,会缓存一定的数据,在此时间之内,可以通过不同的方式来获取此运行时类

获取 Class 实例的几种方式

/**
 * 获取 Class 的实例的方式(前三种需要掌握)
 */
@Test
public void test3() throws ClassNotFoundException {
    // 方式一:调用运行时类的属性:.class
    Class<Person> clazz1 = Person.class;
    System.out.println(clazz1);
    // 方式二:通过运行时类的对象,调用 getClass()
    Person p1 = new Person();
    Class<? extends Person> clazz2 = p1.getClass();
    System.out.println(clazz2);
    // 方式三:调用 Class 的静态方法:forname(String classPath)
    Class<?> clazz3 = Class.forName("cn.tedu.java.Person");
    // clazz3 = Class.forName("java.lang.String");
    System.out.println(clazz3);
    System.out.println(clazz1 == clazz2);
    System.out.println(clazz1 == clazz3);
    // 方式四:使用类的加载器:ClassLoader(了解)
    ClassLoader classLoader = ReflectionTest.class.getClassLoader();
    Class<?> clazz4 = classLoader.loadClass("cn.tedu.java.Person");
    System.out.println(clazz4);
}

创建类的对象的方式

new + 构造器

要创建 Xxx 类的对象,可以考虑:Xxx、Xxxs、XxxFactory、XxxBuilder类中查看是否有静态方法的存在,可以调用其静态方法,创建 Xxx 对象

通过反射

Class 实例可以是哪些结构的说明

class:外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类

interface:接口

[]:数组

enum:枚举

annotation:注解@interface

primitive type:基本数据类型

void

了解 ClassLoader

类的加载过程

类的加载器的作用

**类加载的作用:**将 class 文件字节码内容加载到内存中,并将这些静态数据转换成方 法区的运行时数据结构,然后在堆中生成一个代表这个类的 java.lang.Class 对象,作为 方法区中类数据的访问入口。

**类缓存:**标准的 JavaSE 类加载器可以按要求查找类,但一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间。不过 JVM 垃圾回收机制可以回收这些 Class 对象。

类的加载器的分类

Java 类编译、运行的执行的流程

使用 ClassLoader 加载 src 目录下的配置文件
@Test
public void test2() throws IOException {
    Properties properties = new Properties();
    // 此时的文件默认在当前的 Module 下
    // 读取配置方式一:
    // FileInputStream fileInputStream = new FileInputStream("jdbc.properties");
    // FileInputStream fileInputStream = new FileInputStream("src\\jdbc1.properties");
    // properties.load(fileInputStream);
    // 读取配置方式二:使用 ClassLoader
    // 配置文件默认识别为:当前 module 的 src 下
    ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
    InputStream resourceAsStream = classLoader.getResourceAsStream("jdbc1.properties");
    properties.load(resourceAsStream);
    String user = properties.getProperty("user");
    String password = properties.getProperty("password");
    System.out.println("user = " + user  + ", password = " + password);
}

反射应用一:创建运行时类的对象

代码举例

@Test
public void test1() throws Exception{
    Class<Person> clazz = Person.class;
    Person person = clazz.newInstance();
    System.out.println(person);
}

说明

newInstance():调用此方法,创建对应的运行时类的对象,内部调用了运行时类的空参构造器

要想此方法正常的创建运行时类的对象,要求:

运行时类必须提供空参构造器

空参构造器的访问权限得够,通常设置为 public

在 JavaBean 中要求提供一个 public 的空参构造器,原因:

便于提供反射,创建运行时类的对象

便于子类继承此运行时类时,默认调用 super(),保证父类有此构造器

反射应用二:获取运行时类的完整结构

通过反射,获取对应的运行时类中所有的属性、方法、构造器、父类、接口、父类的泛型、包、注解、异常等

典型代码

@Test
public void test1(){
    Class<Person> clazz = Person.class;
    // 获取属性结构
    // getFeilds():获取当前运行时类及其父类声明为 public 访问权限的属性
    Field[] fields = clazz.getFields();
    for (Field field : fields) {
        System.out.println(field);
    }
    System.out.println();
    // getDeclaredFields():获取当前运行时类中声明的所有的属性(不包含父类中声明的属性)
    for (Field declaredField : clazz.getDeclaredFields()) {
        System.out.println(declaredField);
    }
}
@Test
public void test1(){
    Class<Person> clazz = Person.class;
    // getMethods():获取当前运行时类及其父类声明为 public 访问权限的方法
    Method[] methods = clazz.getMethods();
    for (Method m : methods) {
        System.out.println(m);
    }
    System.out.println();
    // getDeclaredMethods():获取当前运行时类中声明的所有的方法(不包含父类中声明的方法)
    for (Method declaredMethod : clazz.getDeclaredMethods()) {
        System.out.println(declaredMethod);
    }
}
/**
 * 获取构造器
 */
@Test
public void test1(){
    Class<Person> clazz = Person.class;
    // getConstructors():获取当前运行时类中声明为 public 的构造器
    Constructor<?>[] constructors = clazz.getConstructors();
    for (Constructor<?> constructor : constructors) {
        System.out.println(constructor);
    }
    System.out.println();
    // getDeclaredConstructors():获取当前运行时类中声明为 public 的构造器
    Constructor<?>[] declaredConstructors = clazz.getDeclaredConstructors();
    for (Constructor<?> declaredConstructor : declaredConstructors) {
        System.out.println(declaredConstructor);
    }
}
/**
 * 获取运行时类的父类
 */
@Test
public void test2() {
    Class<Person> clazz = Person.class;
    Class<? super Person> superclass = clazz.getSuperclass();
    System.out.println(superclass);
}
/**
 * 获取运行时类的带泛型的父类
 */
@Test
public void test3() {
    Class<Person> clazz = Person.class;
    Type genericSuperclass = clazz.getGenericSuperclass();
    System.out.println(genericSuperclass);
}
/**
 * 获取运行时类的带泛型的父类的泛型
 */
@Test
public void test4() {
    Class<Person> clazz = Person.class;
    Type genericSuperclass = clazz.getGenericSuperclass();
    ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass;
    // 获取泛型类型
    Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
    for (Type actualTypeArgument : actualTypeArguments) {
        // System.out.println(actualTypeArgument.getTypeName());
        System.out.println(((Class) actualTypeArgument).getName());
    }
}
/**
 * 获取运行时类实现的接口
 */
@Test
public void test5(){
    Class<Person> clazz = Person.class;
    Class<?>[] interfaces = clazz.getInterfaces();
    for (Class<?> anInterface : interfaces) {
        System.out.println(anInterface);
    }
    System.out.println();
    // 获取运行时类父类实现的接口
    for (Class<?> anInterface : clazz.getSuperclass().getInterfaces()) {
        System.out.println(anInterface);
    }
}
/**
 * 获取运行时类所在的包
 */
@Test
public void test6(){
    Class<Person> clazz = Person.class;
    Package pack = clazz.getPackage();
    System.out.println(pack);
}
/**
 * 获取运行时类声明的注解
 */
@Test
public void test7(){
    Class<Person> clazz = Person.class;
    Annotation[] annotations = clazz.getAnnotations();
    for (Annotation annotation : annotations) {
        System.out.println(annotations);
    }
}

反射应用三:调用运行时类的指定结构

调用指定的属性

@Test
public void testFeild(){
    try {
        Class<Person> clazz = Person.class;
        // 创建运行时类的对象
        Person p = clazz.newInstance();
        // 1.getDeclaredField(String name):获取运行时类中指明变量名的属性
        Field name = clazz.getDeclaredField("name");
        // 2.保证当前属性是可访问的
        name.setAccessible(true);
        // 3.获取或设置指定属性的值
        name.set(p, "Tom");
        System.out.println(name.get(p));
    } catch (NoSuchFieldException | InstantiationException | IllegalAccessException e) {
        e.printStackTrace();
    }
}

调用指定的方法

@Test
public void testMethod(){
    try {
        Class<Person> clazz = Person.class;
        // 创建运行时类的对象
        Person p = clazz.newInstance();
        /**
         * 1.获取指定的某个方法
         * getDeclaredMethod():参数1:指明获取的方法的名称,参数2:指明获取的方法的形参列表
         */
        Method show = clazz.getDeclaredMethod("show", String.class);
        show.setAccessible(true);
        /**
         * invoke():参数1:方法的调用者 参数2:给方法的形参赋值的实参
         * invoke() 的返回值即为对应类中定义的方法的返回值
         */
        Object returnValue = show.invoke(p, "CHN");
        System.out.println(returnValue);
        Method showDesc = clazz.getDeclaredMethod("showDesc");
        showDesc.setAccessible(true);
        // 如果调用的运行时类中的方法没有返回值,则此 invoke() 返回 null
        // Object returnVal = showDesc.invoke(Person.class);
        Object returnVal = showDesc.invoke(null);
        System.out.println(returnVal);
    } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
        e.printStackTrace();
    }
}

调用指定的构造器

@Test
public void testConstructor(){
    try {
        Class<Person> clazz = Person.class;
        // 创建运行时类的对象
        Person p = clazz.newInstance();
        // 1.获取指定的构造器
        // getDeclaredConstructor():参数:指明构造器的参数列表
        Constructor<Person> constructor = clazz.getDeclaredConstructor(String.class);
        // 2.保证此构造器是可访问的
        constructor.setAccessible(true);
        // 3.调用此构造器创建运行时类的对象
        Person tom = constructor.newInstance("Tom");
        System.out.println(tom);
    } catch (IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException e) {
        e.printStackTrace();
    }
}

反射应用四:动态代理

代理模式的原理

使用一个代理将对象包装起来, 然后用该代理对象取代原始对象。任何对原 始对象的调用都要通过代理。代理对象决定是否以及何时将方法调用转到原始对象上。

静态代理

举例

实现 Runnable 接口的方法创建多线程

// 被代理类
class MyThread implements Runnable{
}
// 代理类
class Thread implements Runnable{
}
public class Main{
    public static void main(String[] args){
        MyThread t = new MyThread();
        Thread thread = new Thread(t);
        // 启动线程;调用线程的 run()
        thread.start(); 
    }
}

静态代理的缺点

代理类和目标 对象的类都是在编译期间确定下来,不利于程序的扩展。

每一个代理类只能为一个接口服务,这样一来程序开发中必然产生过多的代理

动态代理的特点

动态代理是指客户通过代理类来调用其它对象的方法,并且是在程序运行时 根据需要动态创建目标类的代理对象。

动态代理的实现

需要解决两个主要问题

问题一:如何根据加载到内存中的被被代理类,动态的创建一个代理类及其对象

通过 Proxy.newInstance() 解决

问题二:当通过代理类的对象调用方法时,如何动态的去调用被代理类中的同名方法 a

通过 InvocationHandler 接口的实现类及其方法 invoke()

代码实现

public class ProxyTest {
    public static void main(String[] args) {
        SuperMan superMan = new SuperMan();
        // proxyInstance:代理类的对象
        Human proxyInstance = (Human) ProxyFactory.getProxyInstance(superMan);
        // 当通过代理类对象调用方法时,会自动的调用被代理类中同名的方法
        System.out.println(proxyInstance.getBelief());
        proxyInstance.eat("四川麻辣烫");
        System.out.println();
        NickClothFactory nickClothFactory = new NickClothFactory();
        ClothFactory proxyClothFactory = (ClothFactory) ProxyFactory.getProxyInstance(nickClothFactory);
        proxyClothFactory.produceCloth();
    }
}
interface Human{
    String getBelief();
    void eat(String food);
}
/**
 * 被代理类
 */
class SuperMan implements Human{
    @Override
    public String getBelief() {
        return "I believe I can fly";
    }
    @Override
    public void eat(String food) {
        System.out.println("我喜欢吃" + food);
    }
}
class HumanUtil{
    public void method1(){
        System.out.println("通用方法一");
    }
    public void method2(){
        System.out.println("通用方法二");
    }
}
class ProxyFactory{
    /**
     * 调用此方法,返回一个代理类的对象,解决问题一
     * @param obj 被代理类的对象
     * @return
     */
    public static Object getProxyInstance(Object obj){
        MyInvocationHandler handler = new MyInvocationHandler();
        handler.bind(obj);
        return Proxy.newProxyInstance(
                obj.getClass().getClassLoader(),
                obj.getClass().getInterfaces(),
                handler);
    }
}
class MyInvocationHandler implements InvocationHandler{
    /**
     * 需要使用被代理类的对象进行赋值
     */
    private Object obj;
    public void bind(Object obj){
        this.obj = obj;
    }
    /**
     * 当通过代理类的对象,调用方法 a 时,就会自动的调用如下的方法:invoke()
     * 将被代理类要执行的方法 a 的功能声明在 invoke() 中
     * @param proxy
     * @param method
     * @param args
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        HumanUtil humanUtil = new HumanUtil();
        humanUtil.method1();
        // method:即为代理类对象调用的方法,此方法也就做完了被代理对象要调用的方法
        // obj:被代理类的对象
        Object returnValue = method.invoke(obj, args);
        humanUtil.method2();
        // 上述方法的返回值就作为当前类中的 invoke() 的返回值
        return returnValue;
    }
}
体会

反射的动态性

Java8 的其它新特性

Java8 新特性概述

Lambda 表达式

Lambda 表达式使用前后的对比

举例一:

@Test
public void test1(){
    Runnable r1 = new Runnable() {
        @Override
        public void run() {
            System.out.println("我爱北京天安门");
        }
    };
    r1.run();
    System.out.println();
    Runnable r2 = () -> System.out.println("我爱北京故宫");
    r2.run();
}

举例二:

@Test
public void test2() {
    Comparator<Integer> com1 = new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) {
            return Integer.compare(o1, o2);
        }
    };
    int compare1 = com1.compare(12, 21);
    System.out.println(compare1);
    System.out.println();
    // Lambda 表达式的写法
    Comparator<Integer> com2 = (o1, o2) -> Integer.compare(o1, o2);
    int compare2 = com2.compare(32, 21);
    System.out.println(compare2);
    System.out.println();
    // 方法引用
    Comparator<Integer> com3 = Integer::compare;
    int compare3 = com3.compare(32, 21);
    System.out.println(compare3);
}

Lambda 表达式的基本用法

举例:(o1, o2) -> Integer.compare(o1, o2)

格式

->:Lambda 操作符 或 箭头操作符

-> 左边:Lambda 形参列表(其实就是接口中的抽象方法的形参列表)

-> 右边:Lambda 体(其实就是抽象方法的方法体)

如何使用:分为六种情况

总结六种情况:

-> 左边:Lambda 形参列表的参数类型可以省略(类型推断),如果 Lambda 形参列表只有一个城市,其一对() 也可以省略

-> 右边:Lambda 体应该使用一对 {} 包裹,如果只有一条执行语句(可能是 return 语句),可以省略一对 {} 和 return 关键字

函数式接口

使用说明

如果一个接口中,只声明了一个抽象方法,那么此接口称为函数式接口。

可以在一个接口上使用@FunctionalInterface 注解,这样做可以检查它是否是一个函数式接口。

Lambda 表达式的本质:作为函数式接口的实例

4个基本的函数式接口

具体使用:

image.png

总结

何时使用 Lambda 表达式?

当需要对一个函数式接口实例化的时候,可以使用 Lambda 表达式

何时使用给定的函数式接口?

如果开发中需要定义一个函数式接口,首先看看已有的 JDK 提供的函数式接口是否提供了能满足需求的函数式接口,如果有,则直接调用即可,不需要自己再自定义了

方法引用

理解

方法引用可以看做是 Lambda 表达式深层次的表达。换句话说,方法引用就是 Lambda 表达式,也就是函数式接口的一个实例,通过方法的名字来指向一个方法。

使用情境

当要传递给 Lambda 体的操作,已经有实现的方法了,可以使用方法引用

格式

类或对象 :: 方法名

分为三种情况

对象 :: 非静态方法

类 :: 静态方法

类 :: 非静态方法

要求

方法引用使用的要求:要求接口中的抽象方法的形参列表和返回值类型与方法引用的方法的形参列表和返回值类型相同(针对于前两种)

当函数式接口方法的第一个参数是需要引用方法的调用者,并且第二个参数是需要引用方法的参数(或无参数)时:ClassName::methodName

使用建议

如果给函数式接口提供实例,恰好满足方法引用的使用情境,就可以考虑给函数式接口提供实例,如果不熟悉方法引用,那么还可以使用 Lambda 表达式

使用举例

// 情况一:对象 :: 实例方法
// Consumer 中的 void accept(T t)
// PrintStream 中的 void println(T t)
@Test
public void test1() {
    Consumer<String> con1 = str -> System.out.println(str);
    con1.accept("北京");
    PrintStream ps = System.out;
    Consumer<String> con2 = ps::println;
    con2.accept("北京");
}
// Supplier 中的 T get()
// Employee 的 String getName()
@Test
public void test2() {
    Employee emp = new Employee(1001, "Tom", 23, 5000);
    Supplier<String> sup1 = () -> emp.getName();
    System.out.println(sup1.get());
    Supplier<String> sup2 = emp::getName;
    System.out.println(sup2.get());
}
// 情况二:类 :: 静态方法
// Comparator 中的 int compare(T t1, T t2)
// Integer 中的 int compare(T t1, T t2)
@Test
public void test3() {
    Comparator<Integer> com1 = (t1, t2) -> Integer.compare(t1, t2);
    System.out.println(com1.compare(12, 21));
    Comparator<Integer> com2 = Integer::compare;
    System.out.println(com2.compare(32, 21));
}
// Function 中的 R apply(T t)
// Math 中的 Long round(Double d)
@Test
public void test4() {
    Function<Long, Double> fun1 = d -> (double) Math.round(d);
    // Function<Long, Double> fun2 = Math::round;
}
// 情况三:类 :: 实例方法
// Comparator中的int comapre(T t1,T t2)
// String中的int t1.compareTo(t2)
@Test
public void test5() {
    Comparator<String> com1  = (s1, s2) -> s1.compareTo(s2);
    Comparator<String> com2  = String::compareTo;
}
// BiPredicate 中的 boolean test(T t1, T t2);
// String 中的 boolean t1.equals(t2)
@Test
public void test6() {
    BiPredicate<String, String> pre1 = (s1, s2) -> s1.equals(s2);
    BiPredicate<String, String> pre2 = String::equals;
    System.out.println(pre1.test("'", "a"));
    System.out.println(pre1.test("'", "a"));
}
// Function 中的 R apply(T t)
// Employee 中的 String getName();
@Test
public void test7() {
    Function<Employee, String> func1 = e -> e.getName();
    System.out.println(func1.apply(new Employee(1001, "Jerry", 23, 6000)));
    Function<Employee, String> func2 = Employee::getName;
    System.out.println(func2.apply(new Employee(1001, "Jerry", 23, 6000)));
}

构造器引用与数组引用

构造器引用格式

类名 :: new

构造器引用使用要求

和方法引用类似,函数式接口的抽象方法的参数列表和构造器的形参列表一致,抽象方法的返回值类型即为构造器所属的类的类型

构造器引用举例

// 构造器引用
// Supplier 中的 T get()
@Test
public void test1(){
    Supplier<Employee> sup1 = () -> new Employee();
    Supplier<Employee> sup2 = Employee::new;
    System.out.println(sup2.get());
}
// Function 中的 R apply(T t)
@Test
public void test2(){
    Function<Integer, Employee> func1 = id -> new Employee(id);
    Employee employee = func1.apply(1001);
    System.out.println(employee);
    Function<Integer, Employee> func2 = Employee::new;
    System.out.println(func2.apply(1002));
}
// BiFunction 中的 R apply(T t,U u)
@Test
public void test3(){
    BiFunction<Integer, String, Employee> func1 = (id, name) -> new Employee(id, name);
    BiFunction<Integer, String, Employee> func2 = Employee::new;
}

数组引用格式

数组类型[] :: new

数组引用举例

// 数组引用
// Function 中的 R apply(T t)
@Test
public void test4(){
    Function<Integer, String[]> func1 = length -> new String[length];
    String[] arr1 = func1.apply(5);
    System.out.println(Arrays.toString(arr1));
    Function<Integer, String[]> func2 = String[]::new;
    String[] arr2 = func2.apply(5);
    System.out.println(Arrays.toString(arr2));
}

Stream API

Stream API 的理解

Stream 关注的是对数据的运算,与 CPU 打交道

集合关注的是对数据的存储,与内存打交道

Java8 提供了提供了一套 API,使用这套 API 可以对内存中的数据进行过滤、排序、映射、规约等操作,类似于 SQL 对数据库中表的相关操作

注意点

Stream 自己不会存储元素。

Stream 不会改变源对象。相反,他们会返回一个持有结果的新 Stream。

Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行

Stream 的使用流程

Stream 的实例化

一系列的中间操作

终止操作

使用流程的注意点

一个中间操作链,对数据源的数据进行处理

一旦执行终止操作,就执行中间操作链,并产生结果。之后,不会再被使用

步骤一:Stream 实例化

/**
 * 创建方式一:通过集合
 */
@Test
public void test1(){
    List<Employee> employees = EmployeeData.getEmployees();
    // default Stream<E> stream() : 返回一个顺序流
    Stream<Employee> stream = employees.stream();
    // default Stream<E> parallelStream() : 返回一个并行流
    Stream<Employee> parallelStream = employees.parallelStream();
}
/**
 * 创建方式二:通过数组
 */
@Test
public void test2(){
    int[] arr = new int[]{1, 2, 3, 4, 5, 6};
    // static <T> Stream<T> stream(T[] array): 返回一个流
    IntStream stream = Arrays.stream(arr);
    Employee e1 = new Employee(1001, "Tom");
    Employee e2 = new Employee(1001, "Jerry");
    Employee[] employees = {e1, e2};
    Stream<Employee> employeeStream = Arrays.stream(employees);
}
/**
 * 创建方式三:通过 Stream 的 of()
 */
@Test
public void test3(){
    Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6);
}
/**
 * 创建方式四:创建无限流
 */
@Test
public void test4(){
    // 迭代 public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)
    // 遍历前10个偶数
    Stream.iterate(0, t -> t + 2).limit(10).forEach(System.out::println);
    // 生成 public static<T> Stream<T> generate(Supplier<T> s)
    Stream.generate(Math::random).limit(10).forEach(System.out::println);
}

步骤二:中间操作

筛选与切片

image.png

映射

image.png

排序

方法 描述
sorted() 产生一个新流,其中按自然顺序排序
sorted(Comparator com) 产生一个新流,其中按比较器顺序排序

步骤三:终止操作

匹配与查找

image.png

image.png

备注:map 和 reduce 的连接通常称为 map-reduce 模式,因 Google 用它来进行网络搜索而出名。

收集

image.png

Collector 需要使用 Collectors 使用实例

image.png

image.png

Optional 类的使用

理解

为了解决 Java 中的空指针问题而生

Optional 类(java.util.Optional) 是一个容器类,它可以保存类型T的值,代表 这个值存在。或者仅仅保存 null,表示这个值不存在。原来用 null 表示一个值不 存在,现在 Optional 可以更好的表达这个概念。并且可以避免空指针异常。

常用方法

创建 Optional 类对象的方法:

Optional.of(T t) : 创建一个 Optional 实例,t必须非空;

Optional.empty() : 创建一个空的 Optional 实例

Optional.ofNullable(T t):t可以为null

判断 Optional 容器中是否包含对象:

boolean isPresent(): 判断是否包含对象

void ifPresent(Consumer consumer)**:**如果有值,就执行Consumer 接口的实现代码,并且该值会作为参数传给它。

获取Optional容器的对象:

T get(): 如果调用对象包含值,返回该值,否则抛异常

T orElse(T other):如果有值则将其返回,否则返回指定的other对象。

T orElseGet(Supplier other):如果有值则将其返回,否则返回由 Supplier 接口实现提供的对象。

T orElseThrow(Supplier exceptionSupplier) :如果有值则将其返 回,否则抛出由Supplier接口实现提供的异常。

@Test
public void test1(){
    // empty():创建的 Optional 对象内部的 value = null
    Optional<Object> op1 = Optional.empty();
    // Optional 封装的数据是否包含数据
    if (op1.isPresent()) {
        System.out.println("数据为空");
    }
    // 如果 Optional 封装的数据 value 为空,则 get() 报错,否则,value 不为空时,返回 value
    // System.out.println(op1.get());
    System.out.println(op1);
    System.out.println(op1.isPresent());
}
@Test
public void test2(){
    String str = "hello";
    // of(T t):封装数据 t 生成 Optional 对象,要求 t 必须非空,否则报错
    Optional<String> op1 = Optional.of(str);
    // get() 通常与 of() 方法搭配师用,用于获取内部的封装的数据 value
    String str1 = op1.get();
    System.out.println(str1);
}
@Test
public void test3(){
    // ofNullable(T t):封装数据 t 赋给 Optional 内部的 value,不要求 t 非空
    Optional<String> op1 = Optional.ofNullable("beijing");
    // orElse(T t1):如果 Optional 内部的 value 非空,则返回此 value 值,如果 value 为空,则返回 t1
    String str2 = op1.orElse("shanghai");
    System.out.println(str2);
}

典型练习

能确保如下的方法执行中不会出现空指针异常

public String getGirlName2(Boy boy){
    Optional<Boy> boyOptional = Optional.ofNullable(boy);
    Boy boy1 = boyOptional.orElse(new Boy(new Girl("迪丽热巴")));
    Girl girl = boy1.getGirl();
    Optional<Girl> girlOptional = Optional.ofNullable(girl);
    Girl girl1 = girlOptional.orElse(new Girl("古力娜扎"));
    return girl1.getName();
}
@Test
public void test5(){
    Boy boy = null;
    boy = new Boy();
    boy = new Boy(new Girl("苍老师"));
    String girlName = getGirlName2(boy);
    System.out.println(girlName);
}

项目

项目要求

至少独立完成一遍以上的项目代码

积累完成项目的过程中常见的 bug 调试

“硬”看,必要时添加输出语句

Debug

捋顺思路,强化逻辑

对象、数组等内存结构的解析

遵守编码的规范,标识符的命名规范等

在类前,方法前,方法内具体逻辑的实现步骤等添加必要的注释

类前,方法前,属性前:文档注释

逻辑步骤:单行、多行注释

学习的思维方式

大处着眼,小处着手

逆向思维、反证法

透过问题看本质

小不忍则乱大谋

识时务者为俊杰

相关实践学习
基于函数计算快速搭建Hexo博客系统
本场景介绍如何使用阿里云函数计算服务命令行工具快速搭建一个Hexo博客。
目录
相关文章
|
8天前
|
JSON NoSQL Java
Redis入门到通关之Java客户端SpringDataRedis(RedisTemplate)
Redis入门到通关之Java客户端SpringDataRedis(RedisTemplate)
215 0
|
8天前
|
Java
Java编程语言入门指南
Java编程语言入门指南
16 1
|
6天前
|
算法 Java C++
刷题两个月,从入门到字节跳动offer丨GitHub标星16k+,美团Java面试题
刷题两个月,从入门到字节跳动offer丨GitHub标星16k+,美团Java面试题
|
7天前
|
算法 Java Python
保姆级Java入门练习教程,附代码讲解,小白零基础入门必备
保姆级Java入门练习教程,附代码讲解,小白零基础入门必备
|
8天前
|
SQL Java 关系型数据库
零基础轻松入门Java数据库连接(JDBC)
零基础轻松入门Java数据库连接(JDBC)
15 0
|
8天前
|
存储 安全 算法
Java一分钟之-Java集合框架入门:List接口与ArrayList
【5月更文挑战第10天】本文介绍了Java集合框架中的`List`接口和`ArrayList`实现类。`List`是有序集合,支持元素重复并能按索引访问。核心方法包括添加、删除、获取和设置元素。`ArrayList`基于动态数组,提供高效随机访问和自动扩容,但非线程安全。文章讨论了三个常见问题:索引越界、遍历时修改集合和并发修改,并给出避免策略。通过示例代码展示了基本操作和安全遍历删除。理解并正确使用`List`和`ArrayList`能提升程序效率和稳定性。
12 0
|
8天前
|
Java API 开发工具
java与Android开发入门指南
java与Android开发入门指南
17 0
|
8天前
|
Java
Java一分钟之-类与对象:面向对象编程入门
【5月更文挑战第8天】本文为Java面向对象编程的入门指南,介绍了类与对象的基础概念、常见问题及规避策略。文章通过代码示例展示了如何定义类,包括访问修饰符的适当使用、构造器的设计以及方法的封装。同时,讨论了对象创建与使用时可能遇到的内存泄漏、空指针异常和数据不一致等问题,并提供了相应的解决建议。学习OOP需注重理论与实践相结合,不断编写和优化代码。
32 1
|
8天前
|
Java 编译器 对象存储
java一分钟之Java入门:认识JDK与JVM
【5月更文挑战第7天】本文介绍了Java编程的基础——JDK和JVM。JDK是包含编译器、运行时环境、类库等的开发工具包,而JVM是Java平台的核心,负责执行字节码并实现跨平台运行。常见问题包括版本不匹配、环境变量配置错误、内存溢出和线程死锁。解决办法包括选择合适JDK版本、正确配置环境变量、调整JVM内存参数和避免线程死锁。通过代码示例展示了JVM内存管理和基本Java程序结构,帮助初学者更好地理解JDK和JVM在Java编程中的作用。
23 0
|
8天前
|
设计模式 算法 安全
Java多线程编程实战:从入门到精通
【4月更文挑战第30天】本文介绍了Java多线程编程的基础,包括线程概念、创建线程(继承`Thread`或实现`Runnable`)、线程生命周期。还讨论了线程同步与锁(同步代码块、`ReentrantLock`)、线程间通信(等待/通知、并发集合)以及实战技巧,如使用线程池、线程安全设计模式和避免死锁。性能优化方面,建议减少锁粒度和使用非阻塞算法。理解这些概念和技术对于编写高效、可靠的多线程程序至关重要。