JAVA应用程序接口

108 阅读1小时+

异常处理

异常(Exception)是程序执行过程中所产生的问题

异常发生后的解决手段:事前解决、事后解决

异常分类

异常是在JDK中定义的一组专门表示各种不正确情况的类。

一旦发生了对应的不正确情况,那么JVM就会产生该类的对象(异常对象)。

如果,程序没有处理该异常对象,那么这个异常对象会跟随程序的调用流程,一层层往上传递,直到被处理掉。如果到了main方法还没被处理,那么就会结束程序,然后打印异常信息在控制台。

异常信息包括如下几个重要内容:

  1. 发生在哪个线程中

    Exception in thread "main"

    由于目前所学都是单线程的程序,所以现在的异常都发生在thread "main"当中

  2. 发生了什么异常

    异常类的类名通常比较长,但是完整的描述了异常的类型。

  3. 异常发生的代码的位置

    从上往下找自己写的第一行的代码

    异常打印效果如下:

    Exception in Thread “main” 异常类的限定名:异常的详细信息
    at 异常发生的位置(某篇.java:行)
    at 异常发生的位置(某篇.java:行)
    at 异常发生的位置(某篇.java:行)
    at 异常发生的位置(某篇.java:行)
    at 异常发生的位置(某篇.java:行)
    

**在JDK的设计中,异常就是一种类。**它有庞大的继承结构。

最顶级的:throwable

次级:Exception(异常) -- 代码级别可以解决的问题 ---------->需要解决的是异常

Error(错误)-- 系统、环境、硬件等问题

异常的子类:除了RuntimeException(及其子类),其他都是编译时异常。

**运行时异常:**在编译期是不会报的,而是在运行期执行的时候发生。运行时异常一旦发生,代码中又没有处理方案,那么会终止程序的运行。

**编译时异常:**在编译期就会提示这里(方法调用处),有可能出现异常,必须先有处理方案,然后才能编译通过。编译时异常是希望我们在编写的时候就提供方案,否则根本不能运行。

**注意:**编译时编译时异常是指在”编译器”打红线,告知我们这里有未处理的异常;但是,编译期打红线的,还有语法错误等。要区分

**牢记:**只有告知有未处理的“XXException”才是编译时异常

由于继承的关系,我们都知道最重要的方法都是定义在父类当中的。所以,异常类里面我们最常用的方法是定义在Throwable当中的。 其中最重要的两个方法是:

  1. getMessage() --- 该方法是返回这个异常对象中的“详细信息”的。

  2. printStackTrace() --- 该方法是打印异常的堆栈信息,使用的是System.err做的打印。

异常的传播机制

​ 当JVM在运行过程中,一旦发现这里出现异常情况,那么就会自动根据当前异常情况产生对应的异常类对象。然后执行异常的处理方案,如果当前位置没有处理,那么JVM就会结束本方法,然后根据方法调用栈流程转到方法调用处。然后继续查看查看这里没有该异常类的解决方案,如果还是没有,那就继续往上传播。

​ 如果整个代码都没有该异常的解决方案,那么最终的效果就是一定来到main方法检查是否解决,如果还是没有,那么会同样终止main方法的执行。而main方法被最终止,代表程序的被结束,然后JVM关闭前就会把异常信息打印在控制台。

关联知识点:打印异常信息,是调用异常父类Throwable当中的printStackTrace()

异常的处理方案

事前处理

这种处理方案就是让异常根本不发生。在我们写代码的时候就提前把异常出现的可能给解决掉。

几乎所有的运行时异常,都应该采用这种提前做if判断的方式来解决。

事后处理

自己处理(try - catch - finally)
 try{
        正常逻辑下书写的代码,
        但是这个代码有可能发生异常,我们让它试着执行
    
    }catch(要处理的异常类 对象){
        处理异常的代码
    }finally{
        不管是否发生异常都必须要执行的代码
    }

try{}的任务是书写正常逻辑的代码(当然也是有可能发生异常的代码块)

catch(){}的任务是捕获异常,并且书写捕获后要执行的处理动作。注意:就算{}中不写代码,也算把异常捕获了

finally{}的任务是书写不管是否发生异常都要执行的动作,通常是"资源管理","管道关闭","连接的断开"......以前这种动作在软件公司当中,是要求被严格执行的,需要程序员养成一旦产生了要操作的“管道”、“连接”这种资源的时候,不要先写操作代码,而是要先写finally然后在里面关闭它们,再把鼠标跳到中间书写操作代码。 但是,现在不用了,在JDK8之后有一种新的专门针对于需要关闭资源的异常处理代码块,叫做”try with resource“。

组合情况: try - catch try - finally try - catch - finally

1、try-catch try块里面放的是正常逻辑的代码,但这段代码有可能发生异常,所以让他在try当中试着运行。 如果try块中的代码没有发生异常,那么程序流程在执行完try当中所有的代码之后,直接跳到catch块的后面,继续顺序往下执行。 如果try块中的代码发生了异常,那么就会停止try块中后续代码的执行,然后JVM拿着这个异常对象,去和catch块中声明的异常变量进行类型匹配。如果匹配上,JVM就认为你捕获住了异常,那么异常就会认为你已经解决了,就算你在catch块中一句代码都不写,它也认为异常被你解决了,不会再发生终止程序,然后带这异常对象向上传播的情况。

而catch块当中,有没有捕获住异常,不是以来于catch块里面的代码,而是依赖于catch后面的"()"当中声明异常变量的类型!只要这个类型与本次发生的异常对象的类型匹配上,那么就捕获住。

当一个try块里面有可能发生多种异常的时候,那么可以书写多个catch块。每个catch块捕获一种异常。 在进行匹配的时候,是按照从上往下的顺序,依次匹配每个catch块。谁匹配上了就进入谁内部。 如果这多个catch块捕获的异常类型没有继承关系,那么它们也没有谁先谁后的书写顺序。但是一旦有继承关系,那么把捕获子类异常的catch块要写在前面,捕获父类异常的catch块要写在后面

2、try-catch-finally 很明显,是在try-catch后面加了一个finally语句块。 finally语句块,书写在最后一个catch块的后面。它里面代码,在执行流程当中,是表示不管是否发生异常,都必须要执行的代码。

finally当中的这个“必须”,是非常强硬的,也就算遇到return语句。他也必须要执行,在代码级别只有"System.exit(0)"可以阻止它。

try-catch所写的地方

从“让程序不应为异常而崩溃掉”的角度来讲,那么对于我们来说从main方法开始,可以在每层调用处做try-catch,都可以达到这个效果。

但是,在不同层做try-catch,最终执行的效果是不一样的。 你在哪一层进行了处理,那么就从这一层才开始恢复正常,而它之下的全部被忽略掉了正常执行。

在实际应用当中,try-catch真正的难点不是它的语法,而是它在处理以后,场景仍然是一个流畅的。

JDK7之后的特殊语法

在原生语法当中,如果一个try块有可能发生多种异常,那么我们要书写多个catch块,每个catch块截获一种异常。

try{

}catch(异常1){

}catch(异常2){

}catch(异常3){

}

那么JDK7当中,设计了一种新的语法,允许在一个catch块当中,捕获多种异常。

try{

}catch(异常1 | 异常2){

}

注意点:

  1. |符号是分隔异常类型的,而不是分隔两个异常变量;

  2. | 符号两边的异常类型不能有继承关系。因为既然取了父类异常,就没有子类异常的事儿了,因为父类引用可以指向子类对象。

补充:如果finally之前有return语句,那么它到底在什么时候被执行的?
public int test(){
        int a = 10;
        try {
            return a;
        }finally{
            a = 20;
        }
    }

    public static void main(String[] args) {
        int result = new MyClass().test();
        System.out.println(result);
    }
/*
在这种情况下result得到的值不是20,而是10。
因为程序一旦执行到return语句,它并不是马上“先”执行finally。而是先把return的所有准备工作完成,包括返回值的准备。这个时候准备好的返回值是10,然后它才再去执行finally,执行完之后直接做流程的返回。所以finally当中对a的改变,没有影响到return之前准备好的返回值。
*/
交给上层处理(throw throws)

throw 书写在方法的实现体当中是一条执行指令,代表这条指令执行的时候真的会发生一个异常对象的抛出。throw后面跟的是一个异常对象。

throw 异常对象;

throws 书写在方法声明的最后,表示本方法有可能抛出的某些类型的异常。它的后面跟的是异常类的名字。可以接多个异常类的名字,用逗号隔开,没有顺序(注意继承关系)。

两个的关联是:如果在一个方法的内部throw了一个编译时异常对象,那么就要求该方法的声明处必须要有throws。

效果是,调用方法完成调用后,会红线报错,报的是“未处理的异常 某某Exception”。

然后调用者要想解决,那么要么加try-catch;要么继续用throws往上抛。取决于,要解决这个问题的责任者是自己,或自己的再上级调用者。

如何选择是自己处理还是向上抛异常

看这个异常引起的原因,是当前方法本身引起的那么肯定是当前方法自己处理;如果是上层调用者引起的,那就应该抛上去,让调用者处理。

关联知识点:
  1. 一个 完整的方法声明:

    修饰符 返回类型 方法名(形参列表) throws 异常类

  2. 方法重写的规范被增加条件了

    1. 方法名保持一致
    2. 参数列表保持一致
    3. 返回类型保持一致
    4. 访问修饰符重写后不能比重写前小
    5. 重写后不能比重写前抛出更多的异常,这里的更多指的是范围,不是个数。

自定义异常

场景:虽然java的内存库当中,设计了很多异常类以应对各种情况。但是,还是有很多场景中有与业务内容相关的异常,是它不可能涉及到的。所以,这种情况下就需要自定义异常。

语法

  1. 让类继承于Exception或它的某个子类
  2. 给这个类增加带参构造
  3. 还可以给这个异常增加一些工具方法,比如:日志的书写
public class MyException extends Exception {

        public MyException(){

        }

        public MyException(String des){
            super(des);
        }

        public MyException(Throwable throwable){
            super(throwable);
        }

        public String writeLog(){
            return "hello";
        }
}
public class Test {

    public static void main(String[] args) {
        try {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入你的年龄:");
            int age = scanner.nextInt();


        }catch(InputMismatchException inputEx){
            MyException le = new MyException();
            System.out.println(le.writeLog());
        }
    }
}

常用类

字符串

几乎所有的数据值在表达的时候都可以用

字符串的重要性

字符串在我们的编程中是一种非常重要的数据类型。

  1. 使用量大
  2. 它几乎可以表达所有的常见的数据类型。即对于程序外部的使用者而言,所有的交互信息(文本)在本质上都是字符串,他们意识不到其他数据类型的存在。原因:数据类型是程序内部使用的,告知计算机空间划分、数据的存储方式,对于外部是没有意义的。
  3. 字符串操作的场景非常多,所以也需要做一定程度的优化设计。

Java中的字符串类型

在java中一共提供了三种类型可以表示字符串:StringStringBuilderStringBuffer

String
  1. String是唯一设计了字面常量的引用数据类型

    String str = "hello";//hello即字面常量
    System.out.println("请输入:");//请输入:即字面常量
    

    ( 补充:“字面常量”相对于符号常量其实更常用,但也更容易被忽略)

  2. String拥有一个“字符串常量池”的设计

    由于String的使用量非常大,所以对于String的字面量,java在设计的时候专门提供了一个“常量池”来优化。所谓“池”就是预先在内存中放置一系列的对象(字符串常量池就是放的字符串常量对象)。当需要使用的时候,不用临时去创建,而是从池当中取一个来用即可。

    JVM会在加载的时候,把加载到类代码当中,所有书写的字符串常量对象,预存到一个专门的内容空间 -- “字符串常量池”。然后开始执行指令语句,当需要用到这个字符串常量对象的时候,就直接到常量池中去取。

    演示:

    String s0 = "hello";
            String s1 = "hello";
            System.out.println(s0 == s1);//true,与一般的new对象不同--->常量池的运用
    

    此时,在加载期,就会在常量池当中产生一个String类型的对象,里面的值是hello。然后运行起来以后,s0和s1都会被赋值为这个对象的引用。所以用“==”比较的时候,会得到true

    演示:

    String s3 = new String("hello");
    

    由于使用了new的语法,那么会在内存的堆当中产生一个全新的String对象,里面的字符值是hello。

    面试题:

    1.用“==”或“equals”来判断两种String对象的比较结果;

    2.在main方法中,书写“String str = new String("hello");”,创建了几个String对象?创建了几个对象?

  3. String判断非空,应该判断两个条件。

    其他的引用数据类型,只需要判断是否“==”null;但是String有一种特殊性,它可能不为null,但是指向的String对象里面没有存放任何字符数据,是一个空串。所以String判断非空要用两个条件

    //如果输入为空
    //注意判断顺序不能改变,否则会出现空指针异常
    if(str == null || str.equals("")){
        
    }
    //如果输入不为空
    if(str != null && !str.equals(""))
    
  4. String对象的值一旦确定,不能改变

    String的这个特点其实是和String的源代码设计有关。我们可以把String看成是一个封装的char[]

    public final class String{
        private final char[] value;
        /*
        其他属性和一些方法
        */
    }
    

    在这个设计中,我们可以看到字符串中的数据值作为属性存在的,而且该属性是私有的,所以外部不能直接操作,要利用String提供的方法来操作;同时该属性是final的,所以它的值不能被修改。

    到了新版本的JDK8之后,这个char[]被优化成了byte[]。因为不是所有的字符都需要2个字节的空间,很多只需要1个字节空间就可以了。

    这种内容不可更改的特性有会造成一个新的问题,即**当我们大量的在程序中做字符串拼接或需要修改字符串内容的动作时,就会产生很多字符串对象。**所以java又设计了新的字符串类型专门解决这个问题—— 即StringBuilderStringBuffer

String当中的常用方法
  • 与String当中的字符内容有关的方法

    • length() -- 获取字符串当中有多少个字符。--- 平时使用以及面试常见。
    • charAt() -- 根据下标,获取该位置的字符。下标从0开始,如果越界报“StringIndexOutofBoundsException”。
    • indexOf() --- 根据内容,返回该内容在字符串当中出现的位置。内容可以是:char、int、String
    • lastIndexOf() --- 根据内容,返回该内容在字符串当中最后一次出现的位置。indexOf() lastIndexOf() 如果找不到,均返回-1,不会报异常。
    • toCharArray() --- 将字符串的内容构造为一个char[],然后返回出来。
    • toUpperCase();toLowerCase() --- 把字符串的内容转换为全大写或全小写。注意:由于String的内容不可变,所以其实是返回了一个新的字符串。
    • replace() --- 用新的字符或字符串,替换字符串中的老字符或子串。
    • replaceAll() --- 把字符串当中所有满足正则的内容都全部替换。·
    • subString() --- 获取字符串中的部分子串。两个参数代表截取的开始和结束,但是前闭后开的。
  • 与String内容比较有关的

    • equals()
    • equalsIgnoreCase() --- 忽略大小写比较相
    • compareToIgnoreCase() --- 比较两个字符串的大小,忽略大小写。
    • startWith() --- 判断字符串是否以某个子串开头(支持正则
    • endWith() --- 判断字符串是否以某个子串结尾(支持正则)
    • contains() --- 判断字符串是否包含某个子串。
  • 3个特殊方法

    • trim() --- 去掉字符串的前后空格

      当我们在界面上接收了用户的输入以后,很多公司都要求必须要先用这个方法把输入内容的前后无效空格给去掉,然后才能使用。

    • split() --- 把字符串按分隔符进行拆分,返回一个字符串数组。其中分隔符支持正则

    • matches() --- 判断一个字符串是否满足正则表达式(regex).

StringBuilder和StringBuffer
StringBuilder

StringBuilder 是在JDK5当中,提出来的一个辅助String的字符串类型。它最大的特点是:内容可变。 注意:StringBuilder不是String类型,是一种新的类型,所以如下代码都是错的:

StringBuilder sb = "hello";
System.out.println(sb + "world");

由于StringBuilder是为了弥补String在内容不可变上的缺点,所以它提供的方法几乎都是对内容的修改方法。

  1. append()方法

    它的作用是在字符串的尾部添加内容。为了能够将多种数据类型都方便地添加了尾部,提供了大量地重载方法。

  2. delete()方法

    它地作用是在字符串中删除从指定开始下标到结束下标的内容

  3. insert()方法

    它的作用是在字符串中指定位置插入新的内容。它的第一个参数就是插入位置的下标

  4. replace(int start, int end,String str)

    它的作用是在字符串中指定位置的内容替换为新的内容

  5. reverse()

    它的作用是将字符串反转

StringBuffer

StringBuffer也是一个可变的字符串序列,它和StringBuilder的构造、提供的行为完全一样。唯一的区别就是:StringBuffer是线程安全的。

关于线程安全
  1. 线程安全的,效率低
  2. 线程不安全的,效率高
  3. 不是多线程就一定要讲线程安全,多线程的不安全也是在特定条件下才会发生。
使用场景

虽说StringBuilderStringBuffer具有内容可变的特点,但是对于字符串操作来说它们两个提供的方法远不及String的丰富度。

所以这也就限制了它们两个的使用场景,对于程序员来说大部分情况下,还是使用的String。只有当需要“大量”的做字符串内容改变的动作,比如说:大量的拼接、大量的插入、大量的删除...才会考虑另外两个。

所谓“大量”,不是程序员的感官感觉,而是要根据是否影响到了执行效率和内存空间,往往需要测试出来。

结论:对于StringBufferStringBuilder主要掌握内容改变的方法,以备不时之需。更迫切的是掌握String当中的那些常用方法。

正则表达式

​ 正则表达式是一种判定字符串内容是否符合某种规范的表达式,它本身的形式就是一个字符串。 要学习正则表达式,我们首先就要学习正则当中的各种符号:

  1. 任意一个字符串如果不使用正则表达式的特殊符号,那么它也仍然是正则表达式;只不过,这个字符串的所有内容没有变化度,全部被指定死了。

  2. 如果我们要匹配更多的灵活情况,那么我们必须要去学习正则表达式的特殊符号。
    正则表达式在各种编程语言当中都已经实现了,且基本上都是遵循了统一的语法规则(全球标准),只有部分语言自己多提了点特殊语法(用来号称自己更好用而已)。 虽然符号很多,但是抓住三个关键,就是三种括号:

  • [] --- 在正则表达式当中,一个[]代表一个字符,然后把该字符可以有哪些选择,写在它内部。如果在[]内部出现^,那么表示除了这些符号,其他都可以。比如:[^0-9]

  • {} --- 在正则表达式当中,代表它前面的表达式能够出现的次数。

{m,n} --- 至少出现m次,最多出现n次 {m,} --- 至少出现m次,最多不限 {m} --- 只能出现m次

简写形式:?代表 0 - 1次; +代表 至少1次; *代表 任意次

  • () --- 代表可选,可选项之间用“|”号分隔。

正则表达式中正确性比简洁性要重要得多! 在实际开发中,经常使用到的正则表达式的地方很多,通常都是输入有效性的验证: 电话号码、E-mail、邮政编码、身份证......

包装类

在java中两类数据类型:基本和引用。目前来看,这两类数据类型是割裂的,因为无论是它们的存放结构 还有 提供的内容,都让它们是分开的两个体系。

包装类的出现主要的任务就是在基本数据类型和引用数据类型之间搭一个”桥梁“。

4类8种基本类型,各有一个包装类与之对应。

基本数据类型对应包装类
byteByte
shortShort
intInteger
longLong
floatFloat
doubleDouble
charCharacter
booleanBoolean

所有包装类的使用方式都是一样的,只要掌握一个,其他都是一样的。

​ 以Integer为例

public class TestInteger {
    public static void main(String[] args) {
        
        //基本转包装
        int num = 100;
//        Integer in = new Integer(num);    直接调用Integer带参构造(已经过时)
        Integer in = num;  //自动封箱 --> JDK1.5之后的语法 ---语法糖
        in = 1000;

        //包装转基本
        Integer integer1 = 200;
//         int number1 = integer1.intValue();//调用Integer对象的inValue()
        int number1 = integer1;//自动拆箱 --JDK1.5之后的语法糖

        //String转基本
        String str1 = "300";
        int number2 = Integer.parseInt(str1);
        System.out.println(number2);

        //基本转String
        int number3 = 400;
        String str2 = number2+"";//隐式转换
        String str3 = Integer.toString(number3);

        //包装转String
            Integer integer2 = 500;
            String str4 = integer2.toString();
            String str5 = integer1 + "";//也默认调了toString()

        //String转包装
        String str6 = "600";
//        Integer integer3 = new Integer(str6);   JDK9开始过时
        Integer integer4 = Integer.parseInt(str6,8);//把任意进制的数值(字符串类型)转成Integer对象
        System.out.println(integer4);
    }
}

日期、时间类

时间和日期是任何一个程序当中都不可能不涉及的数据,另外在很多加密算法或随机算法种,都涉及到了使用时间来作为计算的种子元素。

时间的本质

在计算机内部,时间的本质不是一个字符串。所见的"某年某月某日"只是它的一个展现形式,这种形式根据不同的需要有很多。

时间在计算机当中,其本质是一个整型(Long类型)数字。它是在时间轴上选择了一个时间点作为”时间元点“,然后把其他时间记录为距离这个时间元点过了多少毫秒。(1s = 1000ms)

初期 -- 时间元点统一为”1900年1月1号00:00:00:000“

现在 -- 时间元点统一推迟到”1970年1月1日00:00:00:000“

在java当中 ,System提供了currentTimeMillis()方法,该方法会返回当前系统记录的毫秒数。应用:在一段代码前后,分别获取毫秒数,然后利用两者之差得到这段代码执行消耗的时间。

System.out.println(System.currentTimeMillis());//返回long类型的数据

Date

最早的在JDK1.0当中就设计出来的日期类型。

//注意来自于java.util包
Date date = new Date();

上面代码就可以获取到代表当前时间的Date对象。

由于Date对象是在95年的时候设计的,当时仍然按照"1900年"作为时间元点,所以时至今日,Date对象虽然仍然能够获取到系统的毫秒数,并构建对象,但是通过毫秒数去计算“年、月、日、时、分、秒”这些动作,全部被标记为“过时”。

这个类到目前仍然在使用,原因:

  1. 这个Date可以在数据库操作的时候与数据库需要用的"java.sql.Date"有更简单的转换方式。

  2. 它是在Java当中最开始提供的时间日期格式化类,是专门操作Date对象的。

public static void testDate(){
        Date now = new Date();//得到目前的时间
        now = new Date(2212313422233l);//也可以使用传入毫秒数的构造
        long mills = now.getTime(); //获取毫秒数,目前应为2212313422233l
        System.out.println(now);//Wed Feb 08 19:30:22 CST 2040
    
        SimpleDateFormat sf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss 第D天  E");
        String msg = sf.format(now);//msg应为 2040年02月08日 19:30:22 第39天  周三
    }

SimpleDateFormat来自于java.text包,这个类的使用比较多。

SimpleDateFormat sf = new SimpleDateFormat("字符串形式的时期格式");
String str = sf.format(Date对象);

现在Date的功能:

  1. 获取当前时间 --- 没问题;

  2. 获取一个指定时间的Date对象 --- 有问题的,只能根据毫秒数构建;

  3. 日期格式化 --- 目前来看没问题

  4. 时间日期的运算操作 --- 有问题,只能获取到谁比谁大(小)。

Calendar

Calendar是在JDK1.1中设计的,其首先任务就是解决Date的时间元点是“1900”的问题,它的时间元点是"1970"。

Calender是一个抽象类,因为日历对于全世界来说有各种历法,Java所实现的“格里高利历”只是一种(公历)。

//获取到代表当前时间的Calendar对象
Calendar cal = Calendar.getInstance();

修改cal对象的数据值

//第一个参数用常量表示你要修改该日历对象的哪个字段,第二个参数就是修改成的新值
cal.set(Calendar.YEAR,2021);
cal.set(Calendar.MONTH,3);
cal.set(Calendar.HOUR,12);

获取cal对象的某项数据值

int year = cal.get(Calendar.YEAR);

在操作Calendar的日历字段的时候,要注意:不是所有的字段都是按“国人”的想法来排列的顺序。比如:月份(Month),1月在Calendar是0,后面的依次推。星期(DAY_OF_WEEK),在Calendar当中,第一个是周天。

所以在设置月份的时候,要记住-1,而获取的时候要记住+1。 获取的时候最建议的方式不是自己去通过Calendar的方法一个一个去获取,最好能利用Date和SimpleDateFormat共同完成。因此,最好掌握把一个Calendar对象转换成Date的对象的API。

//方式一
Date date = new Date(cal.getTimeInMillis());

//方式二
Date date = cal.getTime();

java.time包

java当中的时间日期经历了Date时期,到Calendar时期,都不令人满意。而唯一好用的SimpleDateFormat也出问题了,它线程不安全。而Java目前的应用主要集中在服务器端,所以线程安全考虑是绕不过的。这让Java不得不推出新的时间日期API。

新的时间日期API被放在了java.time包当中。

Instant

Instant在Java当中专门设计的一个代表“瞬时”的类型。之前的JDK中,没有专门的类代表“瞬时”,而是用的long这个数据类型,代表当前毫秒数。 那么这个新的Instant也是其同样作用的,但是它的精度不是毫秒,而是纳秒。1秒 = 1000000000纳秒。

在Instant上有两个属性用来存放精确时间。一个是long类型的seconds,用来存放距离时间元点过了多少秒;一个是int类型的nanos,用来存放秒后面的精度,就是多少纳秒。所以它其实最终表示的方式是:距离时间元点过了"seconds.nanos"。

 public static void TestInstant(){
        Instant in =  Instant.now();//获取瞬时
        System.out.println(in.getEpochSecond());//获取秒 ---这是距离时间元点的秒数
        System.out.println(in.getNano());//获取纳秒 ---这是秒数后面的精度(纳秒)
        System.out.println(in.toEpochMilli());//获取毫秒数 ---距离时间原点的毫秒
        System.out.println(in);
    }

虽然用了Instant来代表某个瞬时,但是利用这个瞬时去计算“年月日,时分秒”是非常不方便的。因此,java.time包当中还专门设计了三个类专用于底层封装Instant,然后提供API供我们操作时间日期。

LocalDate

专用于表示本地“年月日”也就是日期。 常用方法:

//获取当前日期
LocalDate.now();
//获取指定日期
LocalDate.of(年,月,日);
//根据某一天获取它之前或之后的指定日期
LocalDate对象.plusDays(天数);
LocalDate对象.plusMonths(月数);
LocalDate对象.plusYears(年数);
LocalDate对象.minusDays(天数);
LocalDate对象.minusMonths(月数);
LocalDate对象.minusYears(年数);
LocalTime

专用于表示本地时间 常用方法:

//获取当前时间
LocalTime lt = LocalTime.now();
//设置指定的时间
LocalTime when = LocalTime.of(5,30);
//往后调6个小时
lt = lt.plusHours(6);
//往前调10分钟
lt = lt.minusMinutes(10);
LocalDateTime

同时表示本地的日期和时间 常用方法:

//获取当前日期时间
LocalDateTime now = LocalDateTime.now();
//设置指定的日期时间
LocalDateTime when = LocalDateTime.of(2008,5,12,13,07,56);
/*
    也包含各种plus和minus
*/

Properties属性文件

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Properties;

/*
    属性文件操作

    把程序中的内存瞬时数据存入到硬盘上,这个动作---"持久化"。
    持久化的技术很多,最常见的就是两种:1、文件;2、数据库

    属性文件是所有文件操作中最简单的一种。
    属性文件指的是一种特殊格式的文件,它里面的文件内容全是"键=值"的形式存在。

 */
public class TestProperties {

    public static void main(String[] args) {
        Properties props = readFile();

        System.out.println(props.size());//props集合中的元素个数
        //操作集合中的记录
        //1、通过key获取值
        String value = props.getProperty("zhang3");
        value = props.getProperty("wang8");//如果key不存在,返回null
        System.out.println(value);
        //2、通过key修改值
        props.setProperty("zhang3","65");
        System.out.println(props.size());
        props.setProperty("wang8","72");//如果key不存在,相当于新增
        System.out.println(props.size());

        writeFile(props);

    }

    public static Properties readFile(){
        //产生一个Properties对象
        Properties props = new Properties();
        try {
            props.load(new FileInputStream("data/student.properties"));
        } catch (IOException e) {
            e.printStackTrace();
        }

        return props;
    }

    public static void writeFile(Properties props){
        try {
            props.store(new FileOutputStream("data/student.properties"),"");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


}

它是Map下的一个实现类,只不过作为集合类型,它比较弱。因为它的key和值都是String类型,它唯一擅长的就是它除了充当容器,还可以操作一种格式特殊的文本文件(properties属性文件)。

这种特殊格式的属性文件,虽然结构简单,但是在一些不需要存放复杂数据的情况下可以用。

集合框架(JCF)

集合--- 把批量数据集中合并在一起进行操作。

数组是集合的一种,而且是最简单、最原始的一种。体现在三个方面:1、只能存放同一种数据类型元素;2、大小一旦确定不能更改;3、所有元素存放在连续内存地址空间。面向对象的多态特性可以解决1,类的封装性可以解决2,问题3的解决涉及到数据在内存中的存放形式问题(数据结构)。但在JDK当中,已经写好了解决以上3个问题的集合类,而且不止一个,它们构成了java集合框架(JCF)

java中的框架:为了实现某个功能(或效果),预先设计好的一系列具有继承或实现关系的类与接口,开发人员无需从底层开始开发,而是直接在框架的基础上进行二次开发即可。

集合框架的组成

  1. Collection接口时所有集合类的根接口;
  2. List、Set、Map代表了三种结构不同使用场景不同的集合类型。其中List和Set直接继承于Collection,Map间接来自于Collection
  3. 实际使用的时候,用的是List、Set、Map的实现类。需要掌握常见类的使用API
  4. Collections和Arrays是集合框架提供的两个工具类,自带了大量对于批量数据的操作方法(包括:找最大、找最小、排序等)
  5. 比较器(Comparable和Comparator)、迭代器(Iterator)也是用于辅助操作集合内容的接口。
  6. 在集合框架的类设计中,需要了解一种叫做“泛型”的java语法

image-20220426104550923

List(列表)

特点:线性(有序)

往List集合中放置的元素,会根据放的顺序对应它存的顺序。正是因为有序这个特点,List当中的元素都是有下标的,而且所有的下标都是从0开始的。

ArrayList

  • 添加元素 --- add() 注意: 添加的元素默认都是Object类型,基本数据类型进去也是用的“自动封箱”; 可以添加重复的; 可以添加null。

  • 获取元素 --- get() 根据下标去获取该位置的元素; 下标不对,会报异常。

  • 修改元素 --- set(index,value) 根据下标去修改该位置的元素; 下标不对,会报越界异常

  • 删除元素 --- remove() removeAll() 注意: remove是重载方法,可以根据下标删除, 也可以根据元素删除(如果有重复元素,只删除最前面的第一个)。

  • 遍历

    1. 普通for循环

    2. 迭代器循环;

    3. for-each循环

      是专用于从集合类(包括数组)当中,挨个儿每一个元素的循环语法。只要我们不考虑在遍历操作中增/删/跳的动作,只是简单的从头到尾访问,那么使用这个语法比普通for还要简单。 集合框架类的for-each语句在底层中是基于迭代器(Iterator)实现的。

      s  //从集合对象中,每次循环取一个元素赋值给变量
          for(元素类型 变量 : 集合对象){
              //实现对变量的操作
          }
      

ArrayList和Vector的区别

它们都是List集合,底层都是数组,区别在于ArrayList是线程不安全的,而Vector是线程安全的

LinkedList

拥有和ArrayList一模一样的API,甚至执行后的效果也是一样的。但是LinkedList在底层的数据结构上采用了“双向链表”的设计。

LinkedList和ArrayList的区别

  1. 底层区别:前者是双向列表,后者是数组
  2. 效率区别:前者在执行元素的增删动作时效率更高,后者在遍历查询的时候效率更高

Set(集)

特点:不能重复

Set集合中相同的元素只能存放一次,并且由于线性不是它的特点,所以放的顺序和存的顺序不一致,因此在Set当中是没有下标这个概念的。

HashSet判断两个元素重复:

HashSet是通过调用元素的hashcode方法和equals方法来判断两个元素是否重复的。在Java的规范当中要求:两个相同元素理论上应该equals返回true,并且两个元素具有同样的hashcode值 HashSet也是按照这个规范来做的,它会先判断元素的hashcode值是否一直,然后再调用equals方法看是否返回true。

要求:凡是重写了equals方法都应该重写hashcode方法

HashSet

  • 增加元素 -- add()

当我们重复添加一个已经在Set当中存在的元素时,不会报错,只是没有放进去。 允许放入null元素,当然也只能放一个。

  • 获取元素 --- 没有获取某个元素的方法

  • 修改元素 --- 没有修改指定元素的方法

  • 删除元素 --- remove() removeAll()

    remove方法没有重载,只能根据对象进行删除。

  • 获取个数 -- size()

  • 遍历元素

    1. 不能使用普通for循环;
    2. 只能使用for-each语句,从头到尾访问一次。

Map(映射)

Map这种集合,元素是成对存放的,即它的元素是K-V对。Key不能重复,Value是通过Key去映射的

HashMap

HashMap是通过hashcode值保证Key的唯一性的。

增加 --- put() Key和Value都可以为null,但是由于Key不能重复,所以只有一个Key可以为null。

获取 --- get() 通过Key去获取值 如果Key不存在,不会报错,只会返回null。

修改 --- put() 通过已经存在的key,去修改它的值。即没有就增加,有就修改。

删除 --- remove() 通过Key进行删除,如果Key不存在,也不会报错,只是没有执行删除操作而已。

获取个数 --- size()

遍历 --- 需要单独遍历所有的Key或所有的值 keySet() --- 获取所有的Key,返回一个Set集合

Values() --- 获取所有的值,返回一个Collection集合 然后再通过for-each循环遍历每个Key或每个值。

//单独遍历所有的Key
//遍历key
Set<String> keySet = map.keySet();
for (String key: keySet){
	System.out.println(key);
}
// 单独所有的value
//遍历value
Collection<Integer> valueC = map.values();
for (Integer value : valueC){
	System.out.println(value);
}
//判断一个key是否存在
boolean flag = map.containsKey("li1");

可以同时遍历key和value

//Map的一个内部类
for (Map.Entry<Object, Object> entity : properties.entrySet()) {
            String key = (String)entity.getKey();
            String[] value = ((String)entity.getValue()).split("-");
            String password = value[0];
            double account = Double.parseDouble(value[1]);
            allUsers.put(key, new UserBean(key, password, account));
        }

 for (Map.Entry<String, UserBean> entity : allUsers.entrySet()) {
            String userName = entity.getKey();
            UserBean userBean = entity.getValue();
            String value = userBean.getPassword() + "-" + userBean.getAccount();
            properties.setProperty(userName, value);
        }

containsKey() --- 判断一个key是否存在

HashMap 和 Hashtable

HashMap是线程不安全的,允许null作为键和值

HashTable是线程安全的,不允许用null做键或值

Collections工具类

常用方法:

addALL()

ArrayList<String> strLst = new ArrayList<>();
strLst.add("Java");
Collections.addAll(strLst,"hello","world","byebye","badLuck");
System.out.println(strLst.size());//5

max()/min()

String maxStr = Collections.max(strLst);//支持List/Set
String minStr = Collections.min(strLst);//支持List/Set

sort()

Collections.sort(strLst);//排序,只支持List

reverse()

Collections.reverse(strLst);//反转,只支持List

shuffle()

Collections.shuffle(strLst);//混排

Arrays工具类

该工具类与Collections一样提供了很多静态方法,不同的是操作对象是数组。(java.util)

比较器

Comparable接口 -- 内部比较器

被比较对象自己内部实现的比较规则。先人定义的常用类都已经实现了该接口。

内部比较接口应该在被比较对象本身身上实现,然后重写该接口提供的compareTo方法。

compareTo方法的内部实现,记住: 根据在规则下确立的对象位置,分别返回负数、0和正数。 当前对象的位置在传入对象的位置之前,返回“负数”;之后再返回“正数”。

Comparator接口 -- 外部比较器

在比较的时候,通过参数指定比较规则,这个规则与内部比较器同时存在,以外部为准。

单独书写一个比较器类去实现外部比较器,只需要重写compare -- 接口中的其他方法都是default的。

compare方法的内部实现与内部比较器是一样的,相当于用“第一个参数”的位置减去“第二个参数”的位置,分别得到负数、0、正数

泛型

泛型其实早就在其他编程语言当中就以及出现了的,java在JDK1.5之后才实现它的语法。泛型在编程语言中的意义叫做“参数化类型”即“数据类型的参数化”,也就是说有时候在定义一个类或方法的时候,并不能确定要操作的数据类型是哪一个,需要使用者后期在使用过程中告知。那么这个时候,就可以使用 --- ”泛型“的语法来进行设计。

在集合框架中的类型都设计了泛型的语法,用于限制该集合对象只能存放某种数据类型的元素。

 List<String> strList = new ArrayList<>();//JDK8之后的语法
 ArrayList<Interger> intList = new ArrayList<>();

注意:

  1. <>里面只能写一种类型
  2. 可以把泛型设计为放父类类型,这样它的所有子类都可以放入
  3. 泛型只能是引用数据类型,若有这种需求,那么要书写的则是该基本数据类型所对应的包装类

泛型类

假如在一个类的内部,需要使用到某种不确定的类型元素,那么就可以在类的声明处通过"<>"来定义泛型。

public class 类名<T>{
    
}

T只是一个标识符,可以自定义,但通常都写为T、E、K、V 然后在这个类的内部,只要需要用到这个类型的时候,就都可以用这个标识符来进行表达了。可以用它来申明属性、形参,也可以用它来做返回类型,甚至是局部变量的类型。

在定义类的内容时,T由于没有具体的类型(这个还不确定),所以在java中默认它是一个Object类型,所以可以用T定义的变量访问来自Object的方法,但是不能访问某个具体子类的具体行为。注意,在T的外部确定的时候,不能是基本数据类型。

使用的时候

MyClass<某个引用数据类型> mc = new MyClass<>();

mc对象中,所有用到T的地方,就都改变成了在“<>”内所规范的数据类型。

泛型接口

在接口中也可以定义泛型,定义语法和泛型类是一样的。

public interface MyInterface<T> {

 	public void add(T t);

    public T delete();
}	

它的实现类有三种情况: 1、实现类不做泛型设计,那么实现类中的T全部被“擦除”为Object

public class MyClass1  implements MyInterface{

    public void add(Object o) {
        System.out.println(o);
    }
    
    public Object delete() {
        return null;
    }
}

2、实现类根据接口指定的泛型类型

public class MyClass1  implements MyInterface<String>{

    public void add(String o) {
        System.out.println(o);
    }

    public String delete() {
        return null;
    }
}

3、实现类继续使用T,让它的调用者去确定T到底是谁

public class MyClass1<T> implements MyInterface<T>{

    public void add(T o) {
        System.out.println(o);
    }

    public T delete() {
        return null;
    }
}

泛型方法

语法

public <T>返回类型 方法名(T t){
    
}

在调用方法的时候,根据实际参数的类型确定T的类型。支持静态方法和非静态方法,以及多态参数。

java泛型的不足

java的泛型和其他编程语言(特别是C#)的泛型比起来不够强大。

因为java的泛型是假泛型。java为了让他的JVM在运行的时候能够兼容之前的版本,所以java泛型是不会在运行时期做限制的,只是在编译期做了一个编译时的检查。

我们可以根据Java当中的“反射”技术,在运行期绕过它的泛型规范。

I/O

在所有的编程语言当中,I/O操作都是绕不过去的。其中I叫做“输入”--Input,O叫做“输出”--Output。

流模型

在java的I/O设计当中,把输入输出设计成了所谓的“流模型”。“流模型”描述的是,所有的数据传递都是在“数据源”与“目的地”之间,建立一个管道(流类型),然后数据像流水一样通过这根管道进行传递。

所以在操作java的I/O操作时,必须要考虑3个点:数据源、目的地、流类型

I/O流的分类

从传递方向上分为:输入流和输出流

从流类型的粗细上分为:字节流和字符流

由上面的两种分类组合成了I/O流当中的4大父类: 输入字节流:InputStream 输出字节流:OutputStream 输入字符流:Reader 输出字符流:Writer

这4大父类是4个抽象类,他们定义了所有流操作的统一的方法外观,然后让各自的子类去实现自己的内部细节。我们真正要用是用子类。

Java在设计子类的名称的时候,循序了一个很人性化的设计方案,那就是子类名很明确的表示了这个类的使用场景。 比如:FileInputStream -- 数据源是File,目的地是程序,流类型是字节 FileReader -- 数据源是File,目的地是程序,流类型是字符 FileOutputStream -- 数据源是程序,目的地是文件,流类型是字节 FileWriter -- 数据源是程序,目的地是文件,流类型是字符

I/O流的操作步骤

  1. 根据场景选择流类型;

    通过传输的数据选择——如果是文本数据,选择字符流;如果是二进制数据选择字节流。

    通过传递的方向选择输入和输出;通过另一个端点是什么选择合适的子类流。

  2. 产生流对象——new 出该类型的流对象;

  3. 操作流对象——调用read 或 write 方法

  4. 关闭流对象。

注意:一旦涉及到输入输出操作,一定会有各种编译时异常需要我们提前处理。

例子:

public class TestIo {
    public static void main(String[] args) {
        String str = """
                床前明月光,
                疑是地上霜。
                举头望明月,
                低头思故乡。
				""";
//        writerFile(str);
        readFile();
    }
    //输出
    public static void writerFile(String str){
        //如果找不到文件,会去创建一个文件
        FileWriter fw = null;
        try {
            fw = new FileWriter("poem.text");
            fw.write(str);
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (fw != null){
                try {
                    fw.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    //输入
    public static void readFile(){
        FileReader fr = null;
        try {
            fr = new FileReader("poem.text");
            int in = 0;
            //赋值表达式是有唯一返回值的,返回值就是值
            while ((in = fr.read() )!= -1){
                char c = (char) in;
                System.out.print(c);
            }

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fr!=null){
                try {
                    fr.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

文件拷贝

public class CopyFile {
    public static void main(String[] args) {
        FileInputStream  fis = null;
        FileOutputStream fos = null;
        try {
            //从src  --- >tgt
            fis = new FileInputStream("C:\\Users\\ttyy\\Desktop\\src\\a.jpg");
            fos = new FileOutputStream("C:\\Users\\ttyy\\Desktop\\tgt\\a.jpg");

            //每次都从源读取一个byte,然后再把它拷贝到目的地中
//            int b = 0;
//            while ((b = fis.read() )!= -1){
//                fos.write(b);
//            }

            //每次读取一个字节数组,然后再把这个字节数组里面的数据写入目的地中
            int len = 0;
            byte[] b = new byte[1024];
            while ((len = fis.read(b))!= -1){
                fos.write(b,0,len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fis != null){
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
           if (fos != null){
               try {
                   fos.close();
               } catch (IOException e) {
                   e.printStackTrace();
               }
           }
        }
    }

}

try-with-resource

在JDK7当中,提出了新的语法"try-with-resource"。它的效果是当我们在try块中操作需要关闭的资源(不仅仅是针对I/O流操作,包括常用的数据库连接等等)的时候,可以自己去调用close行为。

语法:

try(产生资源对象 -- new动作){
    使用资源对象
}catch(异常){
    异常处理
}

与普通的try-catch-finally在使用的变化有两个:

  1. try后面多打上一对(),里面new资源对象
  2. 不用写资源的关闭动作了。

当需要用到多个要求最终关闭的资源,那么只需要在try的()里面new出多个,它们中间用";"分隔。

 try(new 对象1;new 对象2){
     使用资源对象
}catch(异常){
     异常处理
 }

自定义的资源类如何使用try-with-resource

要想让自定义的资源类同样能够享有try-with-resource的待遇,那么就必须要让这个类实现一个java.lang.AutoCloseable的接口。

该接口只提供了一个方法:close方法,提供实现类重写。同时,这个接口不仅仅只是提供统一的关闭行为外观,还起到了标识接口的作用。

public class MyRs implements AutoCloseable{
    @Override
    public void close(){
        
    }
}

通过实际的测试,我们发现,这个close行为在try块当中执行的时候不管是否发生异常,都一定会被执行,其效果与写在finally当中是一样的。

try-with-resource的本质

它本质是一个语法糖,通过编译后的class文件,将其反编译回来看代码,发现编译后其实没有try-with-resource的语法。

这说明这种语法仅存在于源代码文件中,编译后它还是普通的try-catch语句,然后在try的最后和catch的最后都有一句close的代码。

注意:

  1. 如果在try的()有多个资源,那么这些资源的关闭顺序应该是从后往前的;
  2. 如果使用了多个资源,且这多个资源有对接的效果。那么要注意,最好是给每个资源单独new出来,不要直接使用拼接new对象的方式。如果采用拼接new的方式,那么只有第一根管道会被关闭,后面接的那根会被漏掉;所以最好给单独声明。
  3. try-with-resource当中如果发生异常,那么是先执行close动作,然后再执行catch块当中的代码。而传统的try-catch-finally,如果发生异常,是先执行catch块当中的代码,然后再关闭close动作。
  4. try-with-resource的语法当中,还是允许在最后添加finally块。只不过在这个finally块当中,我们不用再进行关闭资源的动作了,可以根据需求书写其他的”必须要执行的动作“。

对象的序列化与反序列化

在java当中,提供了一种”对象序列化与反序列化“的技术。它指的是:

  1. 对象序列化 --- 把内存中的java对象以二进制流的形式输出出去。
  2. 对象反序列化 --- 把输入进来的对象二进制流直接生成为内存中的一个 对象。

注意:

  1. 无论是序列化与反序列化的概念中,都跟文件没有关系。在它们的概念里面,一个把对象用对象二进制流输出,但没有说输出到哪里去,一个是把输入的对象二进制流转为一个对象,也没有说这个输入流是哪里来的。
  2. 对象的反序列化在java中也是一种产生对象的方式。

序列化API -- ObjectOutputStream

流的分类:

1、按方向:输入流 输出流; 2、按传递单位:字节流 字符流 3、按功能: 节点流 操作流

节点流:比如FileInputStream,它控制了输入字节流(InputStream)的数据源是文件。 FileOutputStream,它控制了输出字节流(OutputStream)的目的地是文件。因此这种管道以及具备了I/O流操作所需要的全部三要素,因此可以直接使用。

ObjectOutputStream它的作用是进行对象序列化,也就是说它的任务是把对象转成二进制流输出,但是没有指定输出到哪里去。那么,也就是说它只是完成了一个转换操作,但还没有确定传输的另一个节点,所以不能单独使用。所以,它被称之为“操作流”。因为操作流是不能够单独直接使用的,所以必须要至少接一个节点流。 因此,我们看到ObjectOutputStream压根儿就没有提供公共无参构造,而是一个公共带参构造,要求至少还要传递一个节点流进去进行流的对接。

//序列化 -- 把对象转成二进制流输出
    public static void testObjectOutputStream(Person person){
        try(FileOutputStream fos = new FileOutputStream("data/person.data");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
        ) {
            /*
            * p对象会先进过oos,转为二进制
            * 然后再通过fos
            * */
            oos.writeObject(person);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
  //写整个hashmap
    public static void testObjectOutputStream(HashMap<Integer,Person> p){
        try( FileOutputStream fos = new FileOutputStream("data/person.data");
              ObjectOutputStream oos = new ObjectOutputStream(fos)){
            /*
                p对象会先进过oos,转为二进制流;
                然后再通过对接fos,输出到文件当中去。
             */
            oos.writeObject(p);

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

反序列化API -- ObjectInputStream

它的操作与对象序列化类似,也是两个流的对接动作。ObjectInputStream只是一个操作流,只负责把对象流转成对象这个操作,而没有控制对象流从哪里来的。所以要对接一个输入的节点流,控制哪个节点作为数据源读数据。

注意:

  1. 如果一个类要参与序列化,那么它应该实现Serializable的接口;并且,它所有的属性都要实现这个接口;JDK自带的常用类,基本上都已经实现了这个接口。

  2. JavaBean的书写规范: 必须要有公共无参构造; 必须为私有属性提供符合命名规范的get/set方法; 应该实现Serializable接口。

  3. 如果有批量数据需要参与序列化,那么就把它们放到一个集合类当中去,然后序列化整个集合对象。JCF当中提供的集合类,自己本身也都实现了Serializable接口。

  4. 序列化以后,不要去修改类的代码,如果修改了,那么反序列化回来的时候,会认为两个的版本的类型不一致了,会报错。

 public static Person testObjectInputStream(){
        Person p = null;
        try(FileInputStream fis = new FileInputStream("data/person.data");
            ObjectInput ois = new ObjectInputStream(fis) ){
            p = (Person) ois.readObject();
//            System.out.println(p.getName() + p.getAge());
            } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }

        return p;
    }

文件类File

File类既可以表示文件又可以表示文件夹

import java.io.File;
import java.io.IOException;

public class TestFile {

    public static void main(String[] args) {
        File f = new File("data");
        boolean flag = f.exists();//判断文件是否存在
        if(flag){
            flag = f.isDirectory();//判断是否是目录
            flag = f.isFile();//判断是否是文件
            if(flag){
                String str = f.getPath();//获取文件相对路径
                str = f.getAbsolutePath();//获取文件绝对路径
                str = f.getName();//获取文件名
                long l = f.length();//获取文件的大小--字节
                long mills = f.lastModified();//获取文件上次修改时间
                f.deleteOnExit();//文件删除 -- 先判断
                f.isHidden();//获取该文件是否是隐藏文件
                System.out.println(mills);
            }else{
                String[] subFileNames = f.list();//罗列该目录下的所有子文件(包括子目录)的名字
                for(String sub : subFileNames){
                    System.out.println(sub);
                }
                File[] subFiles = f.listFiles();//罗列该目录下所有子文件(包括子目录)的File对象
                for(File sub : subFiles){
                    System.out.println(sub.getName() + "  " + sub.length() + "  "
                            + (sub.isFile()?"文件":"目录"));
                }
            }
        }else{
            try {
                f.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

多线程

进程与线程

多任务处理的两种类型:基于进程、基于线程

  • 进程是指一种“自包容”的应用程序,拥有自己独立的内存地址空间。
  • 线程是在进程内部的再次划分,一个进程可以包含多个线程,这些线程是共享进程的内存地址空间。

我们的操作系统在同时运行多个程序,每一个程序就是一个进程;一个程序也可以同时完成多个执行路径,每一个执行路径就是一个线程。(GC就是在JVM中的一个线程,程序从main开始到main结束也是JVM的一个线程)

多线程的同时执行,并不是绝对意义上的同时。而是由CPU在多个线程之间进行来回的切换,并因其切换的速度很快,使用者感受不到它的停顿,所以认为是在同时执行。

线程的生命周期

新建状态 --new出来一个线程对象

就绪状态 -- 启动线程,但是在线程执行自己的内部代码之间。(在启动(start)和运行(run)之间)

运行状态 -- 线程执行自己内部的代码 --- start调用run方法

中断状态 -- 发生在运行过程中,这个线程由于某种原因没有被CPU执行。中断状态又由多个原因造成:由于线程优先级的原因没有抢到CPU;也可以让某个程序主动休眠--- Thread.sleep();线程也有主动退出和换回的行为 --- wait()notify()方法;线程在执行输出或输出的时候进入阻塞状态--- scan.next()

死亡状态 -- 线程执行完成自己的内部代码,然后停下来开始回收自己的资源,进入自毁 --- run方法执行结束。

线程的代码实现

在JDK当中有1个类和2个接口是用来进行多线程编程的:

类---Thread

接口 ---Runnable、Callable(其中Callable是在JDK1.5之后提出来增加Runnable的能力)

Thread

  1. 可以通过继Thread,然后重写run()方法来实现我们自己的线程类。我们可以将run()方法想成是这个线程的主方法。
  2. 在另一个线程中,我们产生它的对象,然后调用它的start()方法。

注意:

如果我们直接调用run(),不会报错。但相当于在做普通的方法调用,这个时候并没有启动一个新的线程。调用start()方法,才会启动新线程,并在新线程中执行run()当中的代码。

  1. Thread类常用方法

sleep()静态方法,用来让当前线程进入休眠状态。参数是毫秒数。一旦休眠时间到了,那么该线程会重新参与CPU的竞争。

setName()

getName() --- 非静态方法,给线程对象设置名字或获取名字

setPriority() --- 非静态方法,给线程设置优先级。总共1-10,默认为5。优先级高的不是一定比优先级低的先执行,只是它获取到CPU的几率更大而已。

Runnable接口

如果一个类已经有一个父类了,又想同时作为线程类,由于java是单继承,所以它不能继承Thread类了。所以,又提供了Runnable接口。

  1. 书写一个线程类实现Runnable接口,重写run()方法;

  2. 在开启它的线程中,把这个Runnable的实现类对象,传给一个Thread对象(在构造方法进行传参),然后调用Thread对象的start()。

Callable接口

无论是继承Thread的方式,还是实现Runnable的方式,都需要重写run()方法。但是run()在设计之初有两个弱点。

  1. run()没有返回;

  2. run()都不能抛异常。

所以在JDK1.5的时候提出了新的Callable接口,该接口的call()方法,可以有返回,也可以抛异常。

1、定义Callable的实现类,然后重写call方法。可以通过Callable的泛型,指定call()的返回类型;

2、在主线程中产生一个FutureTask对象,传入Callable实现类对象;

3、然后再new 出一个Thread对象,传入FutureTask对象;

4、调用Thread的start()方法,执行线程;执行完以后,调用FutureTask对象的get方法获取子线程返回来的数据。

线程安全性

线程安全性发生的时机:当多个线程同时操作同一“资源对象”的时候,才有可能会发生线程安全问题。

解决方案:同步(synchronized)

线程同步。在资源对象上加一把锁(同步锁),让一个线程在执行的时候,其他线程只能进行等待;只有在这个线程执行结束以后,其他线程才能进入。 ---synchronized 关键字

同步方法

当多个线程调用同一个资源对象的方法时,为了解决线程安全,在这个方法的声明处添加一个"synchronized"修饰符,让这个方法成为“同步方法”。

成为同步方法之后的效果是:当一个线程执行这个方法的时候,别的线程只能等着,直到这个方法被该线程执行结束。然后下一个线程再执行,一个一个运行。

注意:synchronized 关键字是加在资源上的, synchronized关键字是作为方法的可选修饰符存在的。

同步块

当多个线程访问同一资源对象的时候,不在资源对象身上加同步,而是让线程内的调用处进行选择是否加同步锁进行调用。如果要加,那么就书写“同步块”。

同步块是在资源方法上不加任何同步修饰,即这个方法既可以普通调用又可以同步调用。

synchronized(资源对象){
    对象.方法1;
    对象.方法2;
}

死锁

当两个线程同时操作一对需要循环依赖的资源的时候,有可能出现死锁的情况。

解决方案是"线程间的通信"。让某个线程主动调用wait()方法进入等待,此时这个线程就退出去了,他同步的资源就释放开了。这个时候另一个线程就可以完成它的任务了。当它完成以后,主动调用notify()方法,前面进入等待的线程就会被唤醒,然后开始执行。

wait()notify()是专门用于线程间通信的方法,来自于Object这个类。 还有一个方法叫做notifyAll(),它是唤醒所有等待的线程,然后让它们进行抢夺。而notify方法只是唤醒第一个调用wait方法的线程对象。

反射(reflect)

反射在java中的地位非常高,有“java灵魂”的称呼。反射技术是允许我们在运行时探究和使用编译时未知的类

根据我们的开发流程都是先书写一个类。然后在代码里面产生它的对象,调用它的方法或属性,然后编译!编译结束以后再运行,就能看到相应的效果 -- 所以这是一个先编译后运行的流程。

但是反射这句话把整个过程给颠倒了,我们是在运行以后才知道我们要产生哪个对象,调用它的哪个方法或属性。比如IDEA这个软件就是用java写的,它在运行起来以后可以探究到我们自己在它运行之后才编译好的类里面有哪些属性和行为,然后可以通过让我们联想的方式可以看到。

这就是java语言提供的动态性效果。这里的动态性指的是“运行”起来以后,对应的“静态性”不是static而是编译后运行前。

编程语言分为:动态语言和静态语言。动态语言可以在程序运行起来之后,给它的类增加或修改或减少属性和行为。当然java作为静态语言做不到。java虽然不是动态语言,但是它具有一定的动态性。这个动态性就体现在“反射”上,虽然它没有办法在运行期给类增加/修改/删除成员内容,但是它可以探究和使用,而这就是反射提供的效果。

要求:

  1. 知道什么是“运行时探究和使用编译时未知的类”,以及其效果。
  2. 掌握基本API。
  3. 在以后使用框架的时候,遇到这种效果,能够知道底层使用的是“反射”,并且能够根据反射的知识去学习和使用这些框架。

java中的反射相关知识点

已知java流程:

  1. 编写java源文件

  2. 编译java源文件为class文件

  3. 运行,而运行又分为3步

    1. 加载
    2. 校验
    3. 解释执行

反射在这几个过程中和“加载”的相关知识很密切,所以我们要深入探究“加载”。

  1. 加载谁?

    加载的是class文件。细节在于:一个java类就会编译成一篇class文件,那么加载class文件就是在加载这个类,所以这个过程也叫做“类加载”

  2. 谁加载?

    在JVM中,提供了一种叫做“类加载器”的东西,来完成加载这个动作。“类加载器”也叫做 ClassLoader,它是JDK当中已经写好了的,提供给JVM用来做加载动作的。

    java在这里还专门设计了一个叫做“双亲委托模型”的东西。java的类加载提供了三种加载器,如果再加上我们自定义的,那就是4中:BootstrapClassLoaderExtClassLoaderApplicationClassLoader、最后是自定义的。

  3. 加载成什么东西?

    class文件是一篇字节码文件,到内存中被加载成了一个对象。这个对象的类型就是Class类型的,所以也被叫做Class对象。Class对象是专门用来装一个类的内容信息的,一个类只需要一个Class对象。

    Student stu = new Student();
    

反射其实就是通过获取这个类的Class对象,我们就能够知道这个类的信息(探究),拿到这些信息以后。。。

反射的基本步骤

  1. 获取一个类的Class对象
  2. 通过Class对象完成一个类的信息的探索。主要是探究3个东西:构造、属性和行为;
  3. 探究到以后,就可以使用这些信息了。探究到构造就可以产生实例对象;探究到属性就可以对属性赋值取值;探究到方法就可以调用。
//一个测试用StudentBean类
public class StudentBean implements Serializable, Comparable<StudentBean> {
    public int id;
    private String name;
    private int age;
    static public String CLASSNAME = "公共1班";

    private StudentBean() {
        System.out.println("StudentBean的公共无参构造");
    }

    private StudentBean(String name){
        this.name = name;
    }

    public StudentBean(String name, int age) {
        this.name = name;
        this.age = age;
        this.id = 9527;
    }

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

    @Override
    public int compareTo(StudentBean o) {
        return 0;
    }
}

获取一个Class对象的反射API

  1. 根据类型名的标识符去获取Class对象--所有的java类型都可以通过这种方式去获取到它的Class对象

    类型名.class

    所有的类型(包括void)都有Class对象;

    基本数据类型可以通过 自己.class,也可以通过自己的 包装类.TYPE 获取到该基本类型的Class对象;

    不管用哪种方式(包括后面的方式2和方式3)获取Class对象,同一类型的只有一个Class对象

    Class stuClass = StudentBean.class;//自定义的java类
    Class strClass = String.class;
    Class interClass = Serializable.class;//接口的Class对象
    Class arrayClass = int[].class;
    Class arrayClass2 = StudentBean[].class;
    Class intClass = int.class; //基本数据类型的Class对象
    Class intClass2 = Integer.TYPE;//也是基本数据类型int的Class对象
    System.out.println(intClass == intClass2)//true
        
    Class voidClass = void.class;//void也有Class对象。
    
    /*
    	在JDK5之前,由于java要体现基本数据类型只能装数据,而不能有任何“.”出来的内容。所以,在语法设计的时候不允许用"int"点出任何内容。那么要获取int的Class的对象,就只能通过int对应的包装类来实现。但是包装类.class得到的是包装类的Class对象;所以又给每个包装类设计了一个TYPE,用于专门获得对应的基本数据类型的Class对象。
    所以:Integer.class和Integer.TYPE得到的是不同的Class对象。
    */
    
  2. 通过实例对象的getClass()获取到Class对象

    在Object这个类当中,除了我们已经熟悉的equalstoStringhashcodewaitnotifynotifyAll,还有一个getClass方法

    getClass()来自Object,所以只有Object的子类才可以使用

    StudentBean stu = new StudentBean("zhang3",18);//StudentBean的实例对象
    Class stuClass1 = stu.getClass();
    Class strClass1 = "hello".getClass();
    Class strClass2 = "world".getClass();
    int[] intArray = {1,3,5,7,9};
    Class intClass3 = intArray.getClass;
    
  3. 通过字符串形式的类型名获取Class对象 -- Class.forName(类型的字符串名称)

    这种方式是需要做异常处理的,因为有可能这个字符串对应的类型找不到,所以forName方法抛出了一个编译时异常ClassNotFoundException。这种方式只能获取类、接口、数组的Class对象,不支持基本数据类型和void。

    其中数组的字符串类型名需要注意:

    引用数据类型数组--"[L元素的类型名",例如 Student[] --> "[Lcom.bean.Student"、 Serializable[] --> "[Ljava.io.Serializable"

    基本数据类型数组--除了long,多是"[类型名的首字母大写"(比如"[I",“[D”,"[B"),只有两个例外:"[J"是long[],“[Z”是boolean[]

    Class stuClass4 = Class.forName("com.bean.StudentBean");//注意使用类全名;会有编译时异常,需要处理
       Class strClass3 = Class.forName("java.lang.String");
       Class interClass2  = Class.forName("java.io.Serializable");
       Class intArrayClass2  = Class.forName("[I");
    
    String str = new Scanner(System.in).next();
       try {
       	 /*
        	Class.forName在编译的时候并不知道要获取谁的Class对象;
       		JVM也不知道要创建谁的Class对象。
       		而是运行起来以后,根据传入的str的值,才能确定要加载谁,
       		然后把谁的Class对象交给c。
       		这才是有动态性的反射代码。
        	*/
       	Class c = Class.forName(str);
       } catch (ClassNotFoundException e) {
                   e.printStackTrace();
       }
    

    三种方式的对比:

    方式1 是可以获取所有的java类型;方式2 只能获取Object的子类类型;方式3 可以获取所有的引用数据类型。

    除了方式3 ,其他两种方式都没有动态性

    String str = new Scanner(System.in).next();
    Class c = Class.forName(str);
    

探究一个Class对象的反射API

//探究在一个类的Class对象中到底存储了哪些东西
Class stuClass = StudentBean.class;
//探究类的基本信息
String className = stuClass.getName();//得到类的全名--类的限定名

className = stuClass.getSimpleName();//得到类的简单名

Class superClass = stuClass.getSuperclass();//得到父类的Class对象
String superName = superClass.getName();
System.out.println("父类名:" + superName);

Class[] interClasses = stuClass.getInterfaces();//得到这个类实现的接口的Class对象
System.out.println("接口名:");
for(Class interC : interClasses){
	System.out.println(interC.getName());
}

Package aPackage = stuClass.getPackage();//得到类所在的包信息
System.out.println("所在包:" + aPackage.getName());

探究构造方法

getConstructors() -- 获取所有的公共构造 getDeclaredConstructors() -- 获取所有声明的构造

这两个方法都是返回的Constructor[]

getConstructor(参数的class对象) -- 获取指定的公共构造 getDeclaredConstructor(参数的class对象) -- 获取指定的声明构造

Constructor[] allPublicCons = stuClass.getConstructors();//获取所有的公共构造

Constructor[] allCons = stuClass.getDeclaredConstructors();//获取所有的构造

Constructor publicCon = stuClass.getConstructor(String.class,int.class);//获取指定的某一个公共构造

Constructor theCon = stuClass.getDeclaredConstructor(String.class);//获取某个指定的与访问修饰符无关的声明构造

for(Constructor con : allCons){
    String conName = con.getName();//构造方法的名字是"类的限定名"
    
    int modNum = con.getModifiers();//构造方法的修饰符,注意返回的是int
    
    String modStr = Modifier.toString(modNum);//得到数值对应的修饰符
    
    Parameter[] params = con.getParameters();//构造方法的参数
    String strParams = "";
    for(int i = 0; i < params.length; i++){
        String parmStr = params[i].getType().getName();//得到参数的类型
        String parmName = params[i].getName();
        strParams += parmStr + " " + parmName;
        if(i != params.length - 1){
            strParams +=",";
        }
    }
    System.out.println(modStr + " " + conName + "(" + strParams + ")");
}

探究属性

getFields() -- 获取所有的公共属性 getDeclaredFields() -- 获取所有声明的属性

这两个方法都是返回的Field[]

getField(属性名) -- 获取某个指定的公共属性 getDeclaredField(属性名) -- 获取某个指定的申明属性

//属性
Field[] allPublicFields = stuClass.getFields();//获取所有的公共属性
Field[] allFields = stuClass.getDeclaredFields();//获取所有的申明属性
Field publicF = stuClass.getField("id");//获取指定的公共属性
Field theF = stuClass.getDeclaredField("CLASSNAME");//获取某一个指定的申明属性
System.out.println(allFields.length);
System.out.println(Modifier.toString(theF.getModifiers()) + " " + theF.getType().getSimpleName() + " " + theF.getName());

探究方法

getMethods() getDeclaredMethods()

这两个方法都是返回的Method[]

getMethod(方法名,参数的class对象) getDeclaredMethod(方法名,参数的class对象)

Method theM = stuClass.getDeclaredMethod("setName",String.class);
System.out.println(theM);

总结

在一个类的Class对象里面封装好了这个类的所有信息(只要你在类的代码里面声明的东西,都可以获取到)。 当然对我们最有意义的,最常用的,就是获取构造、属性和方法的那3组12个get。不要死记,其实就是4个单词和单复数的区别。

使用探究到的信息的反射API

探究到构造,就要产生实例对象

Constructor对象有一个叫做newInstance的方法,通过这个方法可以产生实例对象。

//通过Constrctor产生对象
Class stuClass = StudentBean.class;
//Constructor stuCon1 = stuClass.getDeclaredConstructor();//获取私有无参构造
//StudentBean stu1 = (StudentBean) stuCon1.newInstance();//此处调用这个构造,必然会报异常
//System.out.println(stu1.getName() + "=="  +stu1.getAge());

Constructor stuCon2 = stuClass.getConstructor(String.class, int.class);
StudentBean stu2 = (StudentBean) stuCon2.newInstance("zhang3",28);//需要强转成匹配的类型
System.out.println(stu2.getName() + "=="  +stu2.getAge());

注意

  1. 获取到的Constructor对象的参数要和newInstance方法中传递的参数匹配!!!--- 这个其实就是形参和实参的匹配关系。

  2. 由于我们通过getDeclaredConstructor是可以找到这个类所有的构造方法,但是调用newInstance的时候还是要看该构造的访问修饰符。如果调用处与定义的时候规定的访问权限不匹配,仍然不能产生对象成功。会报:IllegalAccessException

探究到属性,就要给这个属性赋值取值

Field对象身上有一个get()/set()的方法,可以给这个属性进行取值/赋值。当然仍然有前提条件,要遵守这个属性定义的访问修饰符。

//通过Field操作属性
Field f = stuClass.getField("id");

int stuID = (int)f.get(stu2);//取值
System.out.println(stuID);

f.set(stu2,18);//赋值
System.out.println(stu2.id);

//静态属性
Field f2 = stuClass.getField("CLASSNAME");
f2.set(null,"天书1班");
System.out.println(StudentBean.CLASSNAME);
System.out.println(stu2.CLASSNAME);

// Field fAge = stuClass.getDeclaredField("age");
// int stuAge = (int)f.get(stu2);

探究到方法,就要调用这个方法

//通过Method操作方法
Method m = stuClass.getMethod("setAge", int.class);
m.invoke(stu2,23);
System.out.println(stu2.getAge());

Method m2 = stuClass.getMethod("getName");
String returnName = (String)m2.invoke(stu2);
System.out.println(returnName);

总结

Constructor的newInstanceMethod的invokeField的get/set。在操作的时候要注意以下几点:

  1. 调用处的位置要和目标定义的访问修饰符保持一致;

  2. Method和Field的操作要注意和实例对象的绑定关系。

实际使用场景中的反射应用

注解

Annotation 是一种能够被书写到Java源代码中的元数据(metadata:描述数据的数据)形式。类、方法、变量、参数和包都可以使用它来进行描述。Annotation不会对它描述的代码操作有任何直接的影响。

JDK中自带的常用注解

@Override -- 要求编译器在编译时检查被修饰的方法是否是一个正确的重写;

@Deprecated -- 被它修饰的方法会被标记为过时(过时不是错误!),然后编译器就可以提示我们。通常都是用一根斜线把这个方法名划掉。

@SuppressedWarnings -- 用来把代码中的警告信息给消除掉,让它不再报黄色警告。

通过这三种注解的使用,我们了解到了注解的使用表象:

  1. @注解名(参数......),如果一个注解没有参数,那么()可以省略

  2. 注解可以写在类的声明、方法的声明、变量的声明(属性、参数、局部变量)、和包的声明

  3. 注解不仅仅是写给程序员看的,它还可以影响到编译器,甚至是运行期。

注解的本质

Annotation在本质上就是一个Java类型,是JDK1.5提出的。 1、我们可以把Annotation理解为一种特殊的接口,因为所有的Annotation都默认继承了java.lang.annotation.Annotation的接口。但是在实现的语法中,我们不能做直接的接口继承,而是要使用@interface关键字声明它。

public @interface 注解名{
    
}

2、在注解的{}内,我们要填写它的内容。而它的内容既不是方法也不是属性,或者说语法上既像方法又像属性。

public @interface 注解名{
    访问修饰符 数据类型 标识名();
    ......
}

注意 访问修饰符都是public的,可以省略; 数据类型只有5种(基本数据类型、String、Class、枚举、注解类型),以及这5种的一维数组; 标识名应该是名词,因为它代表了一个配置项或者叫做属性名 ()不可少,不是参数列表,仅仅是一种特有语法。 如果某个类型元素有默认值,那么应该是用default接在后面;

3、一个注解定义的时候除了上面两点,还可以控制它的使用位置和获取时机 用@Target注解 定义当前注解的书写位置 用@Retention注解 定义当前注解的获取时机 @Target要求填入的有类声明处、方法声明处、构造声明处、变量声明处、参数声明处等等,而且可以同时配置多个 @Retention只有3个选择:源代码、class文件和运行时。通常都是运行时。

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface 注解名{
    访问修饰符 数据类型 标识名();
    ......
}

另外还有两个修饰注解的注解:@Documented和@Inherited

使用注解

1、首先根据该注解的@Target写在正确的位置;

2、书写的语法:

@注解名(类型元素=值1,类型元素=值2....)

注意 1、如果一个注解没有类型元素,那么可以省略它后面的() @Override @Override() 2、如果一个注解只有一个类型元素,且名字叫做value,那么可以省略"value=" @MyAnnotation({"hello","world"}) @MyAnnotation(value={"hello","world"})

3、数组类型的类型元素,如果只给一个值,那么可以省略掉{} @MyAnnotation("hello") @MyAnnotation({"hello"}) @MyAnnotation(value="hello")

注解的作用

其实,定义注解就是在定义一个具有更强针对性的配置。

注解如何影响运行期

首先一个注解要想在运行期获取到,那么它的@Retention必须是RUNTIME的,然后我们才可以在运行起来以后,通过反射获取到它。

GUI

GUI --- Graphics User Interface

图形用户界面。

Java的GUI技术分三代:

  1. awt --- 它是使用当前操作系统的图形图像库来完成快速的界面实现。但是由于不同操作系统的图形图像库在很多细节上是有差异的,所以导致同一段JavaGUI程序在不同的操作系统上运行会有显示上的差异性。

  2. swing --- 它是Java的第二代GUI库,把第一代库当中可能出现差异性的类给取代调用了,让Java自己去生成它们,比如:我们在前面使用的JOptionPane就是属于swing当中的一个类。所以很长一段时间,Java的GUI开发是使用awt + swing共同实现的,也被称之为swt。

  3. 到了08年左右,SUN公司提出了一套新的GUI技术,被称之为"JavaFX"。它推翻了之前的swt,采用Java对象+CSS样式+XML配置的形式。它在JDK8的时候被纳入到了JDK默认的包当中,但是JDK9的时候又被移除了,成为一个独立的模块,让程序员根据自己的需求进行添加。

注意:不管是JDK中一直自带的awt+swing,还是现在JavaFX,其实用到的场合很少,更多的界面实现还是用的是HTML+CSS+JS的浏览器端开发,只有极少的情况需要用到桌面版的GUI技术。

JavaFX的基本结构和概念

学习网站:www.javafxchina.net/blog/docs/t…

重要名词:

​ Application --- 代表了JavaFX的应用程序

​ Stage --- 舞台 --- 代表了一个窗体。 ​ 在Application的start方法中,提供了一个默认的窗体对象。我们也可以根据自己的需要,new出新的窗体。

​ Scene --- 场景 --- 代表了窗体当中的内容板。 ​ Scene对象在创建的时候要求传参,参数是一个布局。

javafx-sdk-17.0.2

导入步骤:

  1. Open Module Settings----------Libraries--------Java------导入Lib

  2. file-----setting-------Path Variables------+(PATH_TO_FX)

  3. 写类继承Application,重写start方法,run运行

  4. Edit Configrations-----Modify options--------Add VM options----------填入(module-path ${PATH_TO_FX} --add-modules=javafx.controls,javafx.fxml)------main class-----apply

实现多场景转换的API

发生场景转换的触发点:我们在项目当中是利用按钮出发,因此大家要掌握的第一个API,就是给按钮添加事件处理:

按钮对象.setOnAction(new EventHandler(){
    
    @Override
    public void handle(){
    
    }
    
  });

转场景的具体API:

stage.setScene(新场景对象);

OOAD

面向对象分析与设计

设计原则

面向对象是一种编程思维,是我们分析问题设计模型的一种思路,从本质上看,是没有绝对的对与错的。但是不同的设计方式还是有高低之分的,而判断的标准是:

  1. 最基本的标准是功能实现
  2. 在都能够实现功能的前提下,我们的判断标准就叫做“高内聚,低耦合”。

“内聚度” -- 体现的是每一个基本模块自己本身的功能完整度。高内聚指的是一个模块本身的任务就应该在它的内部完成,不要松散到别的地方。

基本模块根据粒度,可以是一个子系统,也可以是一个包,也可以是一个类,还可以是一个方法。

“耦合度” -- 体现的是模块与模块的关联关系,低耦合指的是尽量关联需要使用到的其他模块,无关模块不要关联;关联的时候尽量采用松散易解除或易修改的关联方式。

“高内聚”和“低耦合”

单一原则

模块的职责要单一,不能将太多的职责放在一个模块中。具体到能够理解的范围内:

  1. 一个方法只应该完成一个功能,方法名就应该是对这个功能的完整描述;

  2. 一个类只应该描述一种对象;

  3. 一个包只应该包含跟这个包描述的功能相符的功能类和接口;

开闭原则

开闭原则是其他各种原则的核心,或者说其他的原则都是为了在某一方面达到开闭原则效果而提出来的具体原则。

Software should be opened for Extendtion,but closed for Modification. -- 软件应该对扩展进行开放,对修改进行关闭。

里氏替换原则

这是一个专门用来判断该不该做继承的原则,之前判断该不该做继承用的是一种叫做“is - a”(是一个)的方式。is - a方式是基于我们的生活场景提出来的。但是,在软件系统中,这种经验有可能是错误的。

她提出来判断两个类是否应该做继承的标准是:凡是父类对象在本系统中能够正确工作的位置,都能够替换成子类对象且不引起额外的错误。

依赖倒转原则

在设计类与类的关联的时候,尽量关联对象的抽象(抽象类与接口),不用直接去关联对象的实现类。即上层模块需要绑定下层模块的抽象,不要绑定它的具体实现。 -- 调用者的功能完成需要调用到被调用者的行为(依赖)。形成依赖的话,调用者就需要关联被调用者。关联的目的是为了调用对方的实现行为,来帮被调用者完成自己的实现。

这样做的好处是为了能够达到多态的效果,因为抽象类(接口)的引用可以指向它所有的实现子类对象,如果需要更改实现,只需要增加新的子类或新的实现类,而不需要更改关联处的代码。

public class UserService(){
    private UserDAO dao;
    public boolean login(){
        String username = ......
        String password = ......
        
        User user = dao.getUserByNamePwd(....);
        if(user != null){
           return true;     
        }
        return false;
    }
}

public interface UserDAO{
    public User getUserByNamePwd(String ,String);
}

public class FileUserDAO implements UserDAO{
    //用文件的方式重写getUserByNamePwd
}

public class DBUserDAO implements UserDAO{
    //用了数据库的方式重写
}

接口隔离原则

尽量定义职责单一小接口,少定义职责过多的大接口。

原意:上层接口的设计不应该污染下层接口(或实现类)的职责,要做好职责隔离。

比如:上层接口有三个行为,下层子接口或实现类只需要用到其中的两个或一个。但是通过接口实现的语法,导致下层接口或实现类也有三个行为,多了它们不需要的,这就是“污染”。那么在这种情况下,我们最好做三个上层接口,各自拥有一个行为,让子接口或实现类根据自己的业务需要可以自主选择。

误区一:有人会认为为了不违反接口隔离原则,每个接口都只定义一个行为。如果我们判断出某些行为是同时出现或同时不出现的,完全可以把它们写在一个接口当中。

误区二:接口隔离原则和依赖倒转原则混淆。

在三层架构中,我们的表示类关联业务层接口,然后通过配置去产生业务类对象交给这个接口的引用;同理,我们的业务类关联持久层接口,然后通过配置去产生持久类对象交给这个接口引用。这是典型的“上层不要关联下层的具体实现,而应该关联下层的抽象” -- 依赖倒转。

组合/聚合复用原则

少用继承,多用组合和聚合。

继承的方式是一种硬代码的关联,一旦发生变化,我们必须去修改extends 后面的父类。

组合/聚合是支持多态的,然后再配合反射技术,通过配置文件和注解,可以在不改变java代码的情况下,直接绑定子类对象。

所以从动态性,以及开闭原则来看,组合/聚合比继承好。

迪米特法则

也就“最少知识原则”。

类与类之间的绑定关系,尽量简洁,只绑定与之相关的类,无关的别去绑定。因为绑定得过多,那么引起这类变化得原因也就越多。

模式

关键点:

  1. 模式要用在什么地方
  2. 模式的名字 -- 名字的主要目的是为了更好的在同行中进行交流 -- 名字也是对模式思维精炼描述
  3. 模式的具体解决方案
  4. 模式使用后的优缺点,特别是一个模式有多种解决方案,那就更需要去了解这多种方案的优缺点,才能在合适的时候使用合适的方案

在软件工程当中,模式有两大类:

  1. 设计模式

    更偏向于某个问题域的解决,是代码级别的解决方案 -- 微观

  2. 架构模式

    更偏向于工程的组织架构的设计,对于一个项目中有的众多的类和接口,如何进行分工,如何进行关联 --宏观

设计模式

一共有23种,分为三大类:

  1. 创建模式 -- 主要是创建某种特殊的对象,或者在某种特殊要求下创建对象
  2. 结构模式 -- 主要是利用组合/聚合,或者是继承,让类与类能够形成某种关联结构,从而更适应某个问题域的解决
  3. 行为模式 -- 探讨的是在某种问题域当中如何设计一个行为来适应这个场景
单例模式 --Singleton

当在一个问题域当中,需要我们去创建一种类,这个类能且只能产生一个对象。

  1. 饿汉模式

    /*
    * 单例模式 -- 饿汉模式
    * 缺点:
    *   由于对象的产生是写在静态属性的初始位置;那么只有一加载这个类,不管是否调用getInstance,就算是调用别的静态方法,也会把实例对象产生出来。 ---专业叫法:预加载
    * 优点:
    *   线程绝对安全
    * */
    public class Singleton {
        private static Singleton instance = new Singleton();
        private Singleton(){
            //私有的构造,外部不可能获取到对象
        }
        //static 可以用类名.的方式访问
        public static Singleton getInstance(){
    
            return instance;
        }
    }
    
  2. 懒汉模式

    /*
    * 单例模式 --懒汉模式
    * 优点:
    *   它是延迟加载的,只有当第一次真正调用getInstance方法时才会在内部产生对象
    * 缺点:如果不加控制,那么在多线程情况下,这个方式时线程不安全的,要让懒汉模式做到线程安全,就要给getInstance()加synchronized关键字,让它做到线程同步
    * 线程安全 -- 效率低(多线程情况下会排队)
    * */
    public class Singleton2 {
        private static Singleton2 instance ;
        private Singleton2(){
            //私有的构造,外部不可能获取到对象
        }
        //static 可以用类名.的方式访问
        public static synchronized Singleton2 getInstance(){
            if (instance == null){
                instance = new Singleton2();
            }
            return instance;
        }
    }
    
  3. DCL模式

    /*
    * 单例模型 -- double-Checked locking 机制
    * 优点:
    *   1.延迟加载
    *   2.线程安全
    *   3.性能提升 -- 只有当instance还没有产生的时候,就有多个线程同时调用getInstance()
    *   那么只有在这种情况下线程会排队;
    *   一旦instance产生,就算有多个线程同时调用getInstance(),也不会排队
    *
    * 缺点:
    * JDK5之前,java程序员只能看不能用,当时的JVM的内存模型,不支持这种语法,
    *   因为在java当中 new对象()其实不是一条指令,这些指令不具有原子性,还是可能在多线程的情况下被别的线程插入
    *   在JDK5之后,可以增加一个关键字,告知JVM,这个对象的产生需要原子性,是不可插入的,会让JVM多做以下工作,会部分丢失性能。
    *
    * */
    public class Singleton3 {
        private static volatile Singleton3 instance;
        private Singleton3(){
    
        }
        public static  Singleton3 getInstance(){
            if (instance == null){
                //如果多个线程同时了通过了第一次检查,并且其中一个线程首先通过了第二次检查并实例化了对象,那么剩余通过了第一次检查的线程就不会再去实例化对象。
                synchronized (Singleton3.class){
                    if (instance == null){
                        instance = new Singleton3();
                    }
                }
            }
            return instance;
        }
    }
    
  4. 内部类模式

    /*
        单例模式 --- 静态内部类实现
        1、延迟加载
           只有调用Singleton4的getInstance()的时候,才会用到InnerClass,也才会加载InnerClass,在加载的时候才会产生InnerClass的静态属性instance,也才会产生Singleton4的实例对象。
    
        2、线程安全
           在InnerClass的加载期产生instance对象,运行期所有的线程都只是获取到这个对象,不存在安全性问题。
    
        3、支持高并发
           整个代码中没有使用同步锁,所有线程都可以同时并发运行,不存在排队。
    
         4、唯一的不友好就是对初学者,因为使用了内部类的语法。
     */
    public class Singleton4 {
    
        private Singleton4(){
    
        }
        public static Singleton4 getInstance(){
            return InnerClass.instance;
        }
        private static class InnerClass {
            private static Singleton4 instance = new Singleton4();
        }
    }
    
    
  5. 枚举的方式实现单例

    //使用很少
    public enum Singleton {  
        INSTANCE;  
        public void whateverMethod() {  
        }  
    }
    
工厂模式 -- Factory

使用场景:将对象的使用者和对象的生产者进行分离。

在所有的工厂模式当中,都有三种角色的类,我们把它们称之为“消费者”、“生产者”、“产品”。消费者需要用到产品对象,但是不能自己去构造这个产品对象,而是找生产者要即可;生产者提供方法,专门返回产品对象,无需关心这个产品对象被拿来做什么。

  1. 简单工厂模式

    这种模式考虑的优点是,同一个工厂,可以根据不同的情况,产生不同的产品对象。

    由于变化点是在产品身上,所以这个时候我们往往会抽象产品,然后给它提供不同的实现类

  2. 工厂方法模式

    这种模式同一个产品,不同的工厂生产出来会有差异性。

    这个时候的变化点是在工厂上,所以往往会抽象工厂。

  3. 抽象工厂

    工厂和产品都是变化点,所以既要抽象出工厂父类,也要抽象除产品父类。

原型模式 --- prototype

使用场景:根据一个已经存在的对象,创建一个新的跟它一模一样的对象。 --- 克隆

利用Object中的clone()
  1. 首先要让被克隆的类实现Cloneable接口

  2. 然后重写clone方法,提升它的访问修饰符为public

  3. 最后的效果,是浅克隆。

    所谓的“浅克隆”是指,被clone的对象会产生一个新的,但是它的属性对象不产生。也就是说,clone方法只克隆一层。

    要解决这个问题,只能一层层去克隆,需要我们自己去克隆每一个关联对象,然后把它们组装起来,才能完成“深克隆”。

直接利用对象的序列化和反序列化实现深克隆
  1. 让所有的自定义类型实现Serializable接口
  2. 在最外层的类身上增加一个自定义方法,完成序列化和反序列化就可以了。
总结

原型模式也是后面使用比较多的一种,同时也是面试官的常用问题。 1、深克隆和浅克隆的区别 2、clone方法的使用 3、能够实现对象序列化和反序列化方式的深克隆(代码级别)

架构模式

它的任务是把整个项目的组织结构、包结构、类的职责进行拆分和组合,更利于我们的开发实现,让大家对于各种不同的项目都能有一个基本的套路可循。

重点掌握三层架构

三层架构

任意一个项目,我们都可以从项目的结构中划分出三层:表现层、业务层、持久层

表现层:提供用户视图,用户视图的主要任务是给用户提供操作界面的,即数据的输入和数据的展示。

业务层:主要用途是数据的处理;根据具体项目业务的不同,通过编码方式对数据进行业务操作。

持久层:主要用途是数据的持久化操作,通常都是DB,也有可能是文件。核心操作只有四个-- 增删查改(CRUD)

三层的工作方式

事务脚本模型 -- 每一次用户提交的业务处理都会从表现层-->业务层-->持久层,然后再返回回来。

这样的好处在于:

  1. 在项目的开发中,都不是一个人在完成,而是多人开发,所以形成统一的操作规范是很重要的;
  2. 三层架构首先是对单一职责的体现;根据职责的不同,划分出了三个层各自负责的一个任务,让他们各司其职。
  3. 三层架构的划分也是多态的表现,无论是哪一个层的需求发生了改变,我们都只需要修改该层的内容,而其他层不动。这里体现了分离的特性,不仅仅是代码的分离也是开发人员的分离。
  4. 层与层之间需要做数据的交互,这些数据本质上应该都是对象,这种对象叫做:DTO(数据传输对象)

三层架构提出了它的标准分包结构,然后在各自的包当中提供具备该包规范的各种类;

比如:ATM这个项目

view包 -- javaFX的代码

service包 -- 里面全是业务处理的类和方法

dao包 -- 里面全是对数据的持久操作

bean包 -- 里面全是DTO包,都是具有大量属性的Bean对象;

另外还可能有util包,里面全是工具类。

在分析与设计的流程上:

  1. 首先实现界面,完成界面的基本流程跳转;

  2. 然后通过界面分析有哪些对象?每个对象有哪些属性?然后创建出对应的Bean对象,放到Bean包当中;

  3. 根据要操作的Bean对象,分析出有哪些Service类;根据界面的每次交互,设计出Service有哪些行为?此时先定义成接口,不急着去实现Service

  4. 根据Service当中的各个行为,分析出他们对持久的需要,设计出相应的Dao类和Dao类当中的方法;也是先定义成接口

  5. 完成界面对Service的调用,和Service对Dao的调用,去实现相应的功能

Spring

Spring框架是非常庞大的一个框架,它的使用遍布了整个Java后台应用的开发。

它涉及到了: Spring容器(主要负责对Bean对象的产生和依赖注入,以及AOP),Spring-Web(主要负责是web开发),Spring-boot(主要负责对整个Spring当中各种子框架的搭建),Spring-jpa(主要负责对数据持久操作),Spring-Cloud(主要对微服务进行操作),Spring-Security(对用户的签全,访问安全性,数据安全性)。Spring框架当中最基本一个功能(当然也是最核心的)---Spring容器。

它主要实现了两种技术:IOC 和 AOP。

Spring容器的作用

IOC - 控制反转,也被称为DI(依赖注入)。 AOP - 面向切面编程 -- 它是OOP的一个补充。

Spring容器负责产生对象和管理对象

在代码当中不再需要自己去new对象了,而是通过配置告知Spring容器,他需要产生哪些对象。当我们要用对象的时候,只需要找它要就可以了。

Spring容器管理对象与对象的关联关系

Spring容器当中的对象,还可以根据我们的配置,完成对象与对象的关联关系。

Spring容器的配置

  1. 导入Spring的jar包;-- Maven工具

  2. 会使用@Configration,声明模式注解; 这个模式注解的任务就是做配置,配合@Componet和@ComponentScan

  3. 会使用自动注入的方式,添加依赖(@Autowired)

  4. 能够用AnnotationConfig ApplicationContext类完成Spring上下文加载,以及用getBean方法获取Spring容器中的Bean对象。

举例:利用Spring做一个战士和武器系统

  1. 建立带Spring容器的工程

  2. 掌握把自己写的类,交给Spring去产生对象

    1. 给这个类添加@Component注解;
    2. 书写自定义的注解配置类,使用@Configuration和@ComponentScan
    3. 把武器对象注入到战士对象@Autowired
  3. 能通过上下文取出战士对象,调用攻击行为查看效果;