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文件。先看一下类图

在这里插入图片描述

可以看到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 version="1.0" encoding="UTF-8"?>
  • XML文件必须是以.xml为后缀结尾

XML示例如下:

<?xml version="1.0" encoding="UTF-8" ?>      <!-- 必须有   这是一个注释 -->
<students>
    <student id = "1">
        <name>张三</name>
        <age>18</age>
        <sex></sex>
    </student>

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

    <![CDATA[
        < > % $ # & ! ~
    ]]>
</students>

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

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

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

<data> 3 &lt; 2 &amp;&amp; 5 &gt; 4 </data>

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

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

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

在这里插入图片描述

那么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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hWFuRVIX-1678375912996)(C:\Users\57589\AppData\Roaming\Typora\typora-user-images\image-20230309202928091.png)]

    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文件中的标签只能写<书>、<书名>、<作者>、<售价>这几个标签,如果写其他标签就报错。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O4t5HDnS-1678375912996)(file://E:\a_黑马课程\javaEE课程\day11-特殊文件、日志技术、多线程入门\笔记\assets\1668001422123.png?lastModify=1678365681)]

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

  • DTD约束案例

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

DTD文件解释

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

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

在这里插入图片描述

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

三,日志技术

1,日志概述

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

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

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

日志技术有如下好处

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

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

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

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

在这里插入图片描述

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

Logback日志分为下面几个模块

在这里插入图片描述

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("当前类名");

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

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

日志级别说明
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文件也会记录:

在这里插入图片描述

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):

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

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

5,配置日志的级别

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

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

在这里插入图片描述

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的执行权(注意:哪一个线程先执行,哪一个线程后执行,目前是无法控制的,每次输出结果都会不一样

在这里插入图片描述

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

在这里插入图片描述

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方法,就相当于让乌龟插了一个队,必须乌龟完事了兔子才可以运行,不存在并发。

在这里插入图片描述

有第一个小问题:就是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"),"价格修改");
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yfs1024

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值