java中的特殊文件、日志技术、多线程入门

一,属性文件( .properties)

1,特殊文件概述(必会)

我们知道IO流是用来读数据,目的是为了获取其中的信息供我们使用,但是普通的txt文件是杂乱无章的,除非我们规定,自己写。虽然可以但是约束性不高。所以在java中就出现了一些文件,来供我们使用,也就是只要我们获取到了这样的文件,就可以通过固定的技术来获取其中的内容,得到我们想要的信息。

下面就来了解两种特殊的文本文件,一种是properties文件,一种是XML文件,下面一一的作介绍。

  • 后缀为.properties的文件,称之为属性文件,它可以很方便的存储一些类似于键值对的数据。经常当做软件的配置文件使用。
  • 而xml文件能够表示更加复杂的数据关系,比如要表示多个用户的用户名、密码、家乡、性别等。在后面,也经常当做软件的配置文件使用。

学习这两个文件的方法从几个方面出发:

  1. 了解它们的特点以及作用
  2. 学习使用程序读取他们里面的数据
  3. 学习使用程序把数据存储到这些文件中。

2,Properties属性文件

我们先学习Properties这种属性文件。首先我们要掌握属性文件的格式:

  1. 属性文件后缀以.properties结尾
  2. 属性文件里面的每一行都是一个键值对,键和值中间用 = 隔开。比如: admin=123456
  3. #表示这样是注释信息,是用来解释这一行配置是什么意思。
  4. 每一行末尾不要习惯性加分号,以及空格等字符;不然会把分号,空格会当做值的一部分。
  5. 键不能重复,值可以重复<是不是想到了什么集合 >
#一些有用的信息
#Thu Mar 09 19:52:08 CST 2023
admin=root
pwd=root
白象方便面=8
卫龙辣条=12
华为手机=11996

通过上面对属性文件的简单的介绍,我们看到了这些关键字,键值对,间不能重复,值可以重复。从这里是不是能够想到我们学习的集合。那么如果要是有个类能让我们处理properties文件就好了。

喊一声java天下第一,就有了这么一个类。Properties,用来操作properties文件。先看一下类图

java中的特殊文件、日志技术、多线程入门_第1张图片

可以看到Properties类实现了Map接口,那么肯定就具有map这个双列集合的功能。这样子学习起来就很明白了。

Properties核心作用?

Properties类的对象,用来表示属性文件,可以用来读取属性文件中的键值对。

在了解了他的结构之后就来学习Properties中独有的方法,我们也基本只用他独有的方法

构造器 说明
public Properties() 用于构建Properties集合对象(空容器)
常用方法 说明
public void load(InputStream is) 通过字节输入流,读取属性文件里的键值对数据
public void load(Reader reader) 通过字符输入流,读取属性文件里的键值对数据
public String getProperty(String key) 根据键获取值(其实就是get方法的效果)
public Set stringPropertyNames() 获取全部键的集合(其实就是ketSet方法的效果)

在学习了方法之后就来使用一下,来操作properties文件。使用步骤:

1、创建一个Properties的对象出来(键值对集合,空容器)
2、调用load(字符输入流/字节输入流)方法,开始加载属性文件中的键值对数据到properties对象中去
3、调用getProperty()方法,根据键取值
public static void main(String[] args) throws Exception {
    // 1、创建一个Properties的对象出来(键值对集合,空容器)
    Properties properties = new Properties();
    System.out.println(properties);

    // 2、开始加载属性文件中的键值对数据到properties对象中去
    properties.load(new FileReader("properties-xml-log-app\\src\\users.properties"));
    System.out.println(properties);

    // 3、根据键取值
    System.out.println(properties.getProperty("赵敏"));
    System.out.println(properties.getProperty("张无忌"));

    // 4、遍历全部的键和值。
    //获取键的集合
    Set<String> keys = properties.stringPropertyNames();
    for (String key : keys) {
        //再根据键获取值
        String value = properties.getProperty(key);
        System.out.println(key + "---->" + value);
    }
	
    properties.forEach((k, v) -> {
        System.out.println(k + "---->" + v);
    });
}

**使用Properties往属性文件中写键值对:**需要用到下面的几个方法

常用方法 说明
public Object setProperty(String key, String value) 保存键值对数据到Properties对象中去。
public void store(OutputStream os, String comments) 把键值对数据,通过字节输出流写出到属性文件里去
public void store(Writer w, String comments) 把键值对数据,通过字符输出流写出到属性文件里去

往属性文件中写文件的步骤如下:

1、先准备一个.properties属性文件,按照格式写几个键值对
1、创建Properties对象出来,
2、调用setProperty存储一些键值对数据
3、调用store(字符输出流/字节输出流, 注释),将Properties集合中的键和值写到文件中
	注意:第二个参数是注释,必须得加;
public static void main(String[] args) throws Exception {
    // 1、创建Properties对象出来,先用它存储一些键值对数据
    Properties properties = new Properties();
    properties.setProperty("admin", "张三");
    properties.setProperty("age", "23");
    properties.setProperty("class", "1023");

    // 2、把properties对象中的键值对数据存入到属性文件中去
    properties.store(new FileWriter("properties-xml-log-app/src/users2.properties")
                     , "i saved many message!");

}

运行之后的文件如下:

# i saved many message!         //  这是store 第二个参数加的注释
#Thu Mar 09 20:02:08 CST 2023   //  自动加的时间
admin=张三
age=23
class=1023

二,XML文件

1,XMl文件概述

本质是一种数据的格式,可以用来存储复杂的数据结构,和数据关系。

XML是可扩展的标记语言,意思是它是由一些标签组成 的,而这些标签是自己定义的。本质上一种数据格式,可以用来表示复杂的数据关系。

XML文件有如下的特点:

  • XML中的<标签名> 称为一个标签或者一个元素,一般是成对出现的。
  • XML中的标签名可以自己定义(可扩展),但是必须要正确的嵌套
  • XML中只能有一个根标签。
  • XML标准中可以有属性
  • XML必须第一行有一个文档声明,格式是固定的
  • XML文件必须是以.xml为后缀结尾

XML示例如下:

      
<students>
    <student id = "1">
        <name>张三name>
        <age>18age>
        <sex>sex>
    student>

    <student id = "2">
        <name>李四name>
        <age>20age>
        <sex>sex>
    student>

     % $ # & ! ~
    ]]>
students>

上面XML文件中的数据格式是最为常见的,标签有属性、文本、还有合理的嵌套。XML文件中除了写以上的数据格式之外,还有一些特殊的字符不能直接写。

  • <,>,& 等这些符号不能出现在标签的文本中,因为标签格式本身就有<>,会和标签格式冲突。如果标签文本中有这些特殊字符,需要用一些占位符代替。
<  表示 <
>  表示 >
& 表示 &
' 表示 '
" 表示 "

那么如果需要在文件中表示就需要,像下面一样来对特殊的符号做一个替换:

<data> 3 < 2 && 5 > 4 data>

如果文本中出现了大量的特殊字符,且不想使用特殊字符替换,可以用CDATA区,格式如下:

<data1>
    <![CDATA[
   		3 < 2 && 5 > 4
    ]]>
</data1>

在idea中的XML文件中输入大写的 CD 就会自动生成

java中的特殊文件、日志技术、多线程入门_第2张图片

那么XML在实际的开发中有什么作用呢?

在json还没有出现之前,作为一种特殊的数据结构,在网络中进行传输,但是后面就被json代替了,因为JSON效率更高,更好。

第二就是用在配置文件中,现在仍然在使用。也是我们以后常见的地方。

2,XMl解析1

我们有个一个这样的文本文件,那么我们应该怎样获取中间的数据呢?可以通过前面学习过的IO流,然后一点一点的去获取。但是可以发现十分的麻烦。

记住,如果代码想对来说比较的固定,都是一个套路的话,那么我们的前辈,必然封装过,我们只需要面向对象的调用他们的代码就可以。那么XML解析肯定也有,下面就有一个介绍。

其实有很多开源的,好用的XML解析框架,最知名的是DOM4J(第三方开发的)
由于DOM4J是第三方提供的,所以需要把第三方提供的Jar包导入到自己的项目中来,才可以使用。具体步骤如下:

①下载Dom4j框架,官网下载。

②在项目中创建一个文件夹:lib

③将dom4j-2.1.3.jar文件复制到 lib 文件夹

④在jar文件上点右键,选择 Add as Library -> 点击OK

⑤在类中导包使用

DOM4J解析XML文件的思想是:文档对象模型(意思是把整个XML文档、每一个标签、每一个属性都等都当做对象来看待)。Dowument对象表示整个XML文档、root表示根标签一个XML只有一个根、Element对象表示标签(元素)、Attribute对象表示属性、标签中的内容就是文本。

构造器方法 说明
public SAXReader() 构建Dom4J的解析器对象
public Document read(String url) 把XML文件读成Document对象
public Document read(InputStream is) 通过字节输入流读取XML文件
方法名 说明
Element getRootElement() 获得根元素对象

有根得到Element对象,下面是Element类中的方法:

方法名 说明
public String getName() 得到元素名字
public List elements() 得到当前元素下所有子元素
public List elements(String name) 得到当前元素下指定名字的子元素返回集合
public Element element(String name) 得到当前元素下指定名字的子元素,如果有很多名字相同的返回第一个
public String attributeValue(String name) 通过属性名直接得到属性值
public String elementText(子元素名) 得到指定名称的子元素的文本
public String getText() 得到文本

在dom4j中对元素的操作对象如下,记住图片的中的几个单词基本就会了解析。当然Element中还可以套element

java中的特殊文件、日志技术、多线程入门_第3张图片

    public static void main(String[] args) throws DocumentException {
        // 1. 获取到解析器对象
        SAXReader reader = new SAXReader();
        // 2.得到document对象
        Document read = reader.read(new File("javaEE-day10\\xml\\my.xml"));
	    // 2. 获取到根对象,根中是一个一个的元素
        Element rootElement = read.getRootElement();
	    // 3.  获取到根中的元素遍历
        List<Element> elements = rootElement.elements();
		//   每一个student
        for (Element element : elements) {
//            获取id中的内容
            String id = element.attribute("id").getValue();
            // 得到Student中的每一个元素
            for (Element element1 : element.elements()) {
                // 获取其内容
                String text = element1.getText();
                // 获取属性的名称
                String name = element1.getName();
                System.out.println(id + ": " + name + " - " + text);
            }
        }
    }

4,XML 文件写入

其实这个dom4j中也有写入的方法,其实很没有必要,其实我们做一个字符串然后自己通过IO写入文件即可,不用在创建一堆类,调用一堆方法。我直接写不香吗?所以下一个知识点。

5,XML约束

如果自己亲自的去解析了这个文件,我们会发现,我们需要知道里面有几层标签,这样我们才有法遍历,要不然就是一个XML文件一个代码。那这不带累死。那么有没有一个办法去限制,XML中的内容呢?bing 狗, 当然。就是约束文件啦。

XML约束指的是限制XML文件中的标签或者属性,只能按照规定的格式写。

比如我在项目中,想约束一个XML文件中的标签只能写<书>、<书名>、<作者>、<售价>这几个标签,如果写其他标签就报错。

java中的特殊文件、日志技术、多线程入门_第4张图片

怎么才能达到上面的效果呢?有两种约束技术,一种是DTD约束<老技术>、一种是Schame约束<更新之后>。

  • DTD约束案例

如下图所示book.xml中引入了DTD约束文件,book.xml文件中的标签就受到DTD文件的约束

java中的特殊文件、日志技术、多线程入门_第5张图片

DTD文件解释

<!ELEMENT 书架(书+)>   表示根标签是<书架>,并且书架中有子标签<>
<!ELEMENT 书(书名、作者、售价)> 表示书是一个标签,且书中有子标签<书名><作者><售价>
<!ELEMENT 书名(#PCDATA)>	表示<书名>是一个标签,且<书名>里面是普通文本
<!ELEMENT 作者(#PCDATA)>	表示<作者>是一个标签,且<作者>里面是普通文本
<!ELEMENT 售价(#PCDATA)>	表示<售价>是一个标签,且<售价>里面是普通文本
  • Schame约束案例

如下图所示,左边的book2.xml文件就受到右边schame文件(.xsd结尾的文件)的约束。我们也会发现这个文件可以是一个网址,所以就很方便。我们只需要知道我们应当,怎么做就可以了。

java中的特殊文件、日志技术、多线程入门_第6张图片

第二行中的xmlns 全称是xmlNameSpeace , 可以理解为和java中的import的功用一样,后面的xsi,可以认为是起的一个别名。

三,日志技术

1,日志概述

想搞清楚什么是日志,可以通过下面几个问题来了解的。

  • 系统系统能记住某些数据被谁操作,比如被谁删除了?
  • 想分析用户浏览系统的具体情况,比如挖掘用户的具体喜好?
  • 当系统在开发中或者上线后出现了Bug,崩溃了,该通过什么去分析,定位Bug?

而日志就可以帮我们解决以上的问题。所以日志就好比生活中的日记,日记可以记录生活中的点点滴滴;而程序中的日志,通常就是一个文件,里面记录了程序运行过程中产生的各种数据。

日志技术有如下好处

  1. 日志可以将系统执行的信息,方便的记录到指定位置,可以是控制台、可以是文件、可以是数据库中。
  2. 日志可以随时以开关的形式控制启停,无需侵入到源代码中去修改。

2,日志的体系

有很多日志框架给开发者使用。所谓日志框架就是由一些牛人或者第三方公司已经做好的实现代码,后来者就可以直接拿过去使用。

日志框架有很多种,比如有JUL(java.util.logging)、Log4j、logback等。但是这些日志框架如果使用的API方法都不一样的话,使用者的学习成本就很高。为了降低程序员的学习压力,行内提供了一套日志接口,然后所有的日志框架都按照日志接口的API来实现就可以了。

这样程序员只要会一套日志框架,那么其他的也就可以通过用,甚至可以在多套日志框架之间来回切换。比较常用的日志框架,和日志接口的关系如下图所示

java中的特殊文件、日志技术、多线程入门_第7张图片

下面就来学Logback日志框架,也是业界中使用最为广泛的。

Logback日志分为下面几个模块

java中的特殊文件、日志技术、多线程入门_第8张图片

3,Logback快速入门<必会>

由于Logback是第三方提供的技术,所以首先需要将Jar包引入到项目中,具体步骤如下

  1. 在网上找到slftj-api.jar、logback-core.jar、logback-classes.jar 这三个jar包,复制一下

  2. 在当前模块下面新建一个lib文件夹,把刚刚复制的三个jar包都粘贴到此处

  3. 从资料中找到logback.xml配置文件,将此文件复制粘贴到src目录下(必须是src目录)

    也可以自己创建一个resource目录,但是要和src同级,并且右键选择下图的选项,这样idea就可以识别了,以后什么配置文件呀都在这。

  4. 然后就可以开始写代码了,在代码中创建一个日志记录日对象 <固定 >,通过LOGGER对象操作

logback.xml配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!--
        CONSOLE :表示当前的日志信息是可以输出到控制台的。
    -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <!--输出流对象 默认 System.out 改为 System.err-->
        <target>System.out</target>
        <encoder>
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度
                %msg:日志消息,%n是换行符-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level] %c [%thread] : %msg%n</pattern>
        </encoder>
    </appender>

    <!-- File是输出的方向通向文件的 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
            <charset>utf-8</charset>
        </encoder>
        <!--日志输出路径-->
        <file>D:/javaEE_log/test.log</file>
        <!--指定日志文件拆分和压缩规则-->
        <rollingPolicy
                class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!--通过指定压缩文件名称,来确定分割文件方式-->
            <fileNamePattern>D:/javaEE_log/test-%i-%d{yyyy-MM-dd}-.log.gz</fileNamePattern>
            <!--文件拆分大小-->
            <maxFileSize>1MB</maxFileSize>
        </rollingPolicy>
    </appender>

    <!--
        1、控制日志的输出情况:如,开启日志,取消日志
    -->
    <root level="debug">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="FILE"/>
    </root>
</configuration>
public static final Logger LOGGER = LoggerFactory.getLogger("当前类名");

java中的特殊文件、日志技术、多线程入门_第9张图片

认识一下日志的五个级别:

日志级别指的是日志信息的类型,日志都会分级别,常见的日志级别如下(优先级依次升高):

日志级别 说明
trace 追踪,指明程序运行轨迹
debug 调试,实际应用中一般将其作为最低级别,而 trace 则很少使用
info 输出重要的运行信息,数据连接、网络连接、IO操作等等,使用较多
warn 警告信息,可能会发生问题,使用较多
error 错误信息, 使用较多

通过Logger对象直接调用对应的日志级别即可

示例代码如下:

public class LogBackTest {
    // 创建一个Logger日志对象
    public static final Logger LOGGER = LoggerFactory.getLogger("LogBackTest");

    public static void main(String[] args) {
        //while (true) {
            try {
                LOGGER.info("chu法方法开始执行~~~");
                chu(10, 0);
                LOGGER.info("chu法方法执行成功~~~");
            } catch (Exception e) {
                LOGGER.error("chu法方法执行失败了,出现了bug~~~");
            }
        //}
    }

    public static void chu(int a, int b){
        LOGGER.debug("参数a:" + a);
        LOGGER.debug("参数b:" + b);
        int c = a / b;
        LOGGER.info("结果是:" + c);
    }
}

当我们运行程序时,就可以看到控制台记录的日志:

// 日志输出时间        日志级别 日志在哪个类输出的 线程名  日志记录的信息 
2023-03-09 20:59:46.457 [INFO ] LogBackTest [main] : chu法方法开始执行~~~
2023-03-09 20:59:46.463 [DEBUG] LogBackTest [main] : 参数a:10
2023-03-09 20:59:46.463 [DEBUG] LogBackTest [main] : 参数b:0
2023-03-09 20:59:46.463 [ERROR] LogBackTest [main] : chu法方法执行失败了,出现了bug~~~

会发现配置文件中配置路径中的log文件也会记录:

java中的特殊文件、日志技术、多线程入门_第10张图片

4,日志配置文件logback.xml

源文件在上面

作用: 对Logback日志框架进行控制的。

日志的输出位置、输出格式的设置:

通常可以设置2个输出日志的位置:一个是控制台、一个是系统文件中,通过name可以分别

<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">

开启日志(ALL),取消日志(OFF):


         <appender-ref ref="CONSOLE"/>
         <appender-ref ref="FILE" />
root>
  • 如下图所示,控制日志的输出的格式

日志格式是由一些特殊的符号组成,可以根据需要删减不想看到的部分。比如不想看到线程名那就不要[%thread]。但是不建议更改这些格式,因为这些都是日志很基本的信息。

java中的特殊文件、日志技术、多线程入门_第11张图片

5,配置日志的级别

关于日志的级别,在上面有说明。

那么在哪里配置日志级别呢?如下图所示

java中的特殊文件、日志技术、多线程入门_第12张图片

Logback只输出大于或者等于核心配置文件配置的日志级别信息。小于配置级别的日志信息,不被记录。

配置的是trace,则trace、debug、info、warn、error级别的日志都被输出
配置的是debug, 则debug、info、warn、error级别的日志被输出
配置的是info,则info、warn、error级别的日志被输出
...

四,多线程入门

什么是线程?

线程就是程序内部的一条执行通道。程序中如果只有一条执行通道,那这个程序就是单线程的程序。

那么什么是多线程呢?

多线程是指从软硬件上实现的多条执行流程的技术(多条线程由CPU负责调度执行)。

创建线程的方式有如下三种。常用第二种,但是各有各的好处。

1,线程创建方式1 -> 继承Thread类

具体步骤:

1.定义一个子类继承Thread类,并重写run方法
2.创建Thread的子类对象
3.调用start方法启动线程(启动线程后,会自动执行run方法中的代码)
public class MyThread extends Thread{
    // 2、必须重写Thread类的run方法
    @Override
    public void run() {
        // 描述线程的执行任务。
        for (int i = 1; i <= 5; i++) {
            System.out.println("子线程MyThread输出:" + i);
        }
    }
}

再定义一个测试类,在测试类中创建MyThread线程对象,并启动线程

public class ThreadTest1 {
    // main方法是由一条默认的主线程负责执行。
    public static void main(String[] args) {
        // 3、创建MyThread线程类的对象代表一个线程
        Thread t = new MyThread();
        // 4、启动线程(自动执行run方法的)
        t.start(); 

        for (int i = 1; i <= 5; i++) {
            System.out.println("主线程main输出:" + i);
        }
    }
}

打印结果如下图所示,我们会发现MyThread和main线程在相互抢夺CPU的执行权(注意:哪一个线程先执行,哪一个线程后执行,目前是无法控制的,每次输出结果都会不一样

java中的特殊文件、日志技术、多线程入门_第13张图片

最后我们还需要注意一点:不能直接去调用run方法,如果直接调用run方法就不认为是一条线程启动了,而是把Thread当做一个普通对象,此时run方法中的执行的代码会成为主线程的一部分。此时执行结果是这样的。

java中的特殊文件、日志技术、多线程入门_第14张图片

2,线程创建方式2 -> 实现Runnable接口

Java为开发者提供了一个Runnable接口,该接口中只有一个run方法,意思就是通过Runnable接口的实现类对象专门来表示线程要执行的任务。具体步骤如下

1.先写一个Runnable接口的实现类,重写run方法(这里面就是线程要执行的代码)
2.再创建一个Runnable实现类的对象
3.创建一个Thread对象,把Runnable实现类的对象传递给Thread
4.调用Thread对象的start()方法启动线程(启动后会自动执行Runnable里面的run方法)

代码如下:先准备一个Runnable接口的实现类

/**
 * 1、定义一个任务类,实现Runnable接口
 */
public class MyRunnable implements Runnable{
    // 2、重写runnable的run方法
    @Override
    public void run() {
        // 线程要执行的任务。
        for (int i = 1; i <= 5; i++) {
            System.out.println("子线程输出 ===》" + i);
        }
    }
}

再写一个测试类,在测试类中创建线程对象,并执行线程

public class ThreadTest2 {
    public static void main(String[] args) {
        // 3、创建任务对象。
        Runnable target = new MyRunnable();
        // 4、把任务对象交给一个线程对象处理。
        //  public Thread(Runnable target)
        new Thread(target).start();

        for (int i = 1; i <= 5; i++) {
            System.out.println("主线程main输出 ===》" + i);
        }
    }
}

控制台输出结果:

主线程main输出 ===1
主线程main输出 ===2
主线程main输出 ===3
子线程输出 ===1
子线程输出 ===2
子线程输出 ===3
子线程输出 ===4
子线程输出 ===5
主线程main输出 ===4
主线程main输出 ===5

3,线程创建方式2 之匿名内部类 和 Lambda表达式

关于匿名内部类和Lambda,在前面的笔记中都有记录,所以就不在赘述,直接使用

public class ThreadTest2_2 {
    public static void main(String[] args) {
        // 1、直接创建Runnable接口的匿名内部类形式(任务对象)
        Runnable target = new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 5; i++) {
                    System.out.println("子线程1输出:" + i);
                }
            }
        };
        new Thread(target).start();

        // 简化形式1:匿名内部类
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 5; i++) {
                    System.out.println("子线程2输出:" + i);
                }
            }
        }).start();

        // 简化形式2:lambda表达式
        new Thread(() -> {
                for (int i = 1; i <= 5; i++) {
                    System.out.println("子线程3输出:" + i);
                }
        }).start();

        for (int i = 1; i <= 5; i++) {
            System.out.println("主线程main输出:" + i);
        }
    }
}

4,线程的创建3 -> 实现Callable接口

已经有两种了为什么还有要第三种呢? 这样,我们先分析一下前面两种都存在的一个问题。然后再引出第三种可以解决这个问题。

  • 假设线程执行完毕之后有一些数据需要返回,前面两种方式重写的run方法均没有返回结果。
public void run(){
    ...线程执行的代码...
}
  • JDK5提供了Callable接口和FutureTask类来创建线程,它最大的优点就是有返回值。在Callable接口中有一个call方法,重写call方法就是线程要执行的代码,它是有返回值的
public T call(){
    ...线程执行的代码...
    return 结果;
}

第三种创建线程的方式,步骤如下

1.先定义一个Callable接口的实现类,重写call方法
2.创建Callable实现类的对象
3.创建FutureTask类的对象,将Callable对象传递给FutureTask
4.创建Thread对象,将Future对象传递给Thread
5.调用Threadstart()方法启动线程(启动后会自动执行call方法)call()方法执行完之后,会自动将返回值结果封装到FutrueTask对象中
6.调用FutrueTask对的get()方法获取返回结果

代码如下:先准备一个Callable接口的实现类

public class CallableTest implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 0; i <= 100; i++) {
            sum += i;
        }
        return sum;
    }
}

再定义一个测试类,在测试类中创建线程并启动线程,还要获取返回结果

public class ThreadTest3 {
    public static void main(String[] args) throws Exception {
        // 3、创建一个Callable的对象
        Callable<String> call = new MyCallable(100);
        // 4、把Callable的对象封装成一个FutureTask对象(任务对象)
        // 未来任务对象的作用?
        // 1、是一个任务对象,实现了Runnable对象.
        // 2、可以在线程执行完毕之后,用未来任务对象调用get方法获取线程执行完毕后的结果。
        FutureTask<String> f1  = new FutureTask<>(call);
        // 5、把任务对象交给一个Thread对象
        new Thread(f1).start();


        Callable<String> call2 = new MyCallable(200);
        FutureTask<String> f2  = new FutureTask<>(call2);
        new Thread(f2).start();


        // 6、获取线程执行完毕后返回的结果。
        // 注意:如果执行到这儿,假如上面的线程还没有执行完毕
        // 这里的代码会暂停,等待上面线程执行完毕后才会获取结果。
        String rs = f1.get();
        System.out.println(rs);

        String rs2 = f2.get();
        System.out.println(rs2);
    }
}

再定义一个测试类,在测试类中创建线程并启动线程,还要获取返回结果

public class CallableTestMain {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 3、创建一个Callable的对象
        CallableTest callableTest = new CallableTest();
        // 4、把Callable的对象封装成一个FutureTask对象(任务对象)
        // 未来任务对象的作用?
        // 1、是一个任务对象,实现了Runnable对象.
        // 2、可以在线程执行完毕之后,用未来任务对象调用get方法获取线程执行完毕后的结果。
        FutureTask<Integer> task = new FutureTask<>(callableTest);
        // 5、把任务对象交给一个Thread对象
        new Thread(task).start();

        // 6、获取线程执行完毕后返回的结果。
        // 注意:如果执行到这儿,假如上面的线程还没有执行完毕
        // 这里的代码会暂停,等待上面线程执行完毕后才会获取结果。
        Integer integer = task.get();
        System.out.println("最终的结果是:"+integer);
        System.out.println("main方法结束");
    }
}

5,三种方式的优点和缺点

方式一优缺点:

  • 优点: 编码简单

  • 缺点: 线程类已经继承Thread,无法继承其他类,不利于功能的扩展。

方式二优缺点:

  • 优点: 任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强。

  • 缺点: 需要多一个Runnable对象。

方式三优缺点:

  • 优点: 线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;可以在线程执行完毕后去获取线程执行的结果。

  • 缺点: 编码复杂一点。

6,Thread类常用的方法

Thread提供的常见构造器 说明
public Thread(String name) 可以为当前线程指定名称
public Thread(Runnable target) 封装Runnable对象成为线程对象
public Thread(Runnable target, String name) 封装Runnable对象成为线程对象,并指定线程名称
Thread提供的常用方法 说明
public void run() 线程的任务方法
public void start() 启动线程
public String getName() 获取当前线程的名称,线程名称默认是Thread-索引
public void setName (String name) 为线程设置名称
public static Thread currentThread() 获取当前执行的线程对象
public static void sleep(long time) 让当前执行的线程休眠多少毫秒后,再继续执行
public final void join()… 让调用当前这个方法的线程先执行完!

下面演示一下getName()setName(String name)currentThread()sleep(long time)join()这些方法的使用效果。

public class ThreadMethodTest {
    public static void main(String[] args) {
//        使用lambda表达式传入一个Runnable接口实现的匿名类, 通过构造器给线程起名叫做  “乌龟-----”
        Thread wuGuiThread = new Thread(() -> {
            for (int i = 1; i <= 100; i++) {
//                通过Thread.currentThread()静态方法获取当前的Thread对象, 然后获取线程的名称。
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }, "乌龟-----");
//        开启乌龟这个线程
        wuGuiThread.start();

//      通过构造器给线程起名叫做  “兔子”
        Thread tuZiThread = new Thread(() -> {
            for (int i = 1; i <= 100; i++) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
//               让这个“兔子”,每次休眠1毫秒      为了测试join这个就先注释了
//                try {
//                    Thread.sleep(1);
//                } catch (InterruptedException e) {
//                    e.printStackTrace();
//                }
//                i到15的时候,让乌龟通过Join这个方法进来插队
                if (i == 15){
                    try {
//                        这里必须等乌龟的执行完了兔子的才可以执行
                        wuGuiThread.join();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "兔子");
//        开启兔子这个线程
        tuZiThread.start();
    }
}

使用join之后的测试结果:也就是说,使用了join方法,就相当于让乌龟插了一个队,必须乌龟完事了兔子才可以运行,不存在并发。

java中的特殊文件、日志技术、多线程入门_第15张图片

有第一个小问题:就是start之后java做了什么?

线程对象.start()  ———>  start0()jvm机调用 --- 再根据操作系统的算法进行调用线程

Thread类还提供了诸如:yield、interrupt、守护线程、线程优先级等线程的控制方法,在开发中很少使用,这些方法会后续需要用到的时候再补充。

五 properties文件的小案例

有这样的一个product.properties文件: 使用代码把下面的product.properties文件中所有商品的价格都放大2倍,文件内容如下:(文件自己创建)

#价格修改
#Fri Mar 10 10:43:34 CST 2023
白象方便面=12
卫龙辣条=8
华为手机=20000

使用properties的方法如下:

public class Homework04 {
    public static void main(String[] args) throws IOException {
        Properties properties = new Properties();
//        读取properties文件
        properties.load(new FileReader("javaEE-day10\\product.properties"));
//        获取到KeySet集合
        Set<String> strings = properties.stringPropertyNames();
//        遍历key
        for (String product : strings) {
//            通过key得到value
            String price = properties.getProperty(product);
//            再写回去
            properties.setProperty(product, Integer.parseInt(price) * 2 + "");
        }
//        写入properties文件中
        properties.store(new FileWriter("javaEE-day10\\product.properties"),"价格修改");
    }

既然用了流, 考虑一下,如果使用前面的stream流应该怎么处理呢? 下面的处理上面的需求比较麻烦,但是是对前面Stream流知识的一个复习;还是很有必要的.

public class Homework04OtherMethodByStream{
    public static void main(String[] args) throws IOException {
        Properties properties = new Properties();
        properties.load(new FileReader("javaEE-day10\\product.properties"));

//        获取entry对象 即一个一个的键值对对象
        Set<Map.Entry<Object, Object>> entries = properties.entrySet();
//        对entry中的数据进行处理   map的作用是对每个对象做一个转换.这里我们就是修改他们的值
        Map<Object, Object> collect = entries.stream().map(entry -> {
//           修改每一个value并写入
            Integer integer = Integer.parseInt((String) entry.getValue()) * 2;
            entry.setValue(integer + "");
            return entry;
//            收集一下数据流
        }).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
        System.out.println(collect);

        //        写入properties文件中
        properties.store(new FileWriter("javaEE-day10\\product.properties"),"价格修改");
    }

你可能感兴趣的:(java,开发语言)