Java-01-源码篇-04集合-04-Properties

admin2024-07-08  59

目录

一,简介

二,源码讲解

2.1 Properties 继承结构

2.2 Properties 属性分析

2.3 Properties 构造器

2.4 Properties 加载配置资源文件

 2.4.1 load(inputStream) 

2.4.2 load(Reader)

2.4.3 load0(LineReader lr)

2.4.4 load测试

2.5 Properties 保存配置资源文件

2.5.1 save(out, comments)

2.5.2 store(OutputStream, comments)

2.5.3  store0(BufferedWriter bw, comments, escUnicode)

 2.5.4 测试保存资源配置文件

2.6 Properties 加载 XML 配置资源文件

2.6.1 loadFromXML(InputStream)

2.6.2 测试 Properties 加载 XML 资源配置文件

2.7 Properties 保存 XML 配置资源文件

2.7.1 storeToXML(outputStream, comment)

 2.7.2 storeToXML(OutputStream, comment, encoding)

2.7.3 storeToXML(OutputStream, comment, Charset)

2.7.4 测试 Properties 保存 XML 配置文件

2.8 Properties 设置资源文件

2.8.1 测试类型强转异常

2.9 Properties 获取资源文件

2.9.1 getProperty(key, defaultValue)

2.9.2 getProperty(key)

2.9.3 获取Properties全部配置文件的keys

2.9.4 list(PrintStream) 遍历Properties 配置文件资源

2.9.5 list(PrintWriter) 遍历Properties 配置文件资源

2.9.6 enumerate(map) &  enumerateStringProperties 遍历

2.10 其他方法都是 ConcurrentHashMap 用于替换 HashTable 重写相关Map集合方法


一,简介

       java.util.Properties 是Java 里面一个工具类;Properties类表示一组持久的属性。属性可以保存到流中,也可以从流中加载。属性列表中的每个键及其对应的值都是字符串。

        Java中对于读取application.propeties等类型的配置文件,原生通过IO开始读取并解析获取对应的key和value,再添加到map集合中进行管理维护整个过程相对于比较繁琐。

        所以,Java内部工具类库 java.util 就自带一个properties 类;该类已经写好读取和写入properties类型的配置文件业务逻辑方法;并且也提供对应的map集合进行管理着配置文件的数据。

public class Properties extends Hashtable<Object,Object> {}

         properties 的继承类来自于 Hashtable 父类;从所周知,Hashtable 类已经被遗弃了,因为在Map集合处理线程安全方面有着 JDK 1.5 提供的 ConcurrentHashMap;还有在JDK 1.2 有一个合集工具类Collections 将普通Map封装成一个线程安全的Map集合

Java-01-源码篇-04集合-04-Properties,第1张

二,源码讲解

2.1 Properties 继承结构

Java-01-源码篇-04集合-04-Properties,第2张

package java.util;

/**
 * @author  Arthur van Hoff
 * @author  Michael McCloskey
 * @author  Xueming Shen
 * @since   1.0
 */
public class Properties extends Hashtable<Object,Object> {/** 忽略代码*/}

        尽管Properties 继承了 Hashtable 类,但是现在的Properties已经脱离了 Hashtable 类被线程安全类 ConcurrentHashMap 所替代,具体怎么替代的?

JDK1.8 源码如下:

Java-01-源码篇-04集合-04-Properties,第3张

        JDK 8 是直接调用 Hashtable 父类的 put方法 

 Java-01-源码篇-04集合-04-Properties,第4张 

JDK17 源码:

Java-01-源码篇-04集合-04-Properties,第5张

Java-01-源码篇-04集合-04-Properties,第6张

        JDK17 将所有的map常见方法都进行了重写,并委托给了 map 

Java-01-源码篇-04集合-04-Properties,第7张        其实从这里,就可以得出了在 JDK17 就已经通过 ConcurrentHashMap 替换调 Hashtable;方式就是在其内部引入  ConcurrentHashMap 并将用到 Hashtable 父类的map方法全部重写,并且调用方向都是 ConcurrentHashMap;

        这种方式也是通过`组合`的方式进行设计。并且借助 ConcurrentHashMap 还提高了性能和并发性

2.2 Properties 属性分析

package java.util;
public class Properties extends Hashtable<Object,Object> {
    /** 用于序列化的版本控制,确保在反序列化时类的版本兼容 */
    @java.io.Serial
    private static final long serialVersionUID = 4112578634029874840L;

    /** 提供低级别的、不安全的操作。获取实例的方法是受限的,通常只有特权代码才允许使用。*/
    private static final Unsafe UNSAFE = Unsafe.getUnsafe();

    /** 用于存储默认值。如果在当前属性集中找不到键,则会从默认属性集中查找。 */
    protected volatile Properties defaults;

    /** 
     * ConcurrentHashMap 用于替代 Hashtable 集合管理内容 并且还提高性能和并发性 。
     * map 声明为 transient,在序列化时,这个字段将不会被序列化。这意味着需要自定义序列化逻辑以确保属性的正确保存和恢复
     */
    private transient volatile ConcurrentHashMap<Object, Object> map;
    /** 忽略其他代码 */
}

2.3 Properties 构造器

【无参构造】:默认大小为8

    public Properties() {
        this(null, 8);
    }

【单参构造】:指定初始容量

    public Properties(int initialCapacity) {
        this(null, initialCapacity);
    }

【单参构造】:指定默认的配置文件

    public Properties(Properties defaults) {
        this(defaults, 8);
    }

 【双参构造】:指定默认的配置文件和初始容量

    private Properties(Properties defaults, int initialCapacity) {
        /*
         * 从这里也可以看出,Hashtable 被 ConcurrentHashMap 所代替,直接传入 null;
         * 而不是传入初始容量 initialCapacity 
         * (Void) null 将 null 强转成 Void 类型,是为了避免Hashtable 有多个构造器,编译器选择错误,所以强转成 Void ; 
         * Void 可以理解成 void 类型的包装类,表示不可实例化的队列
         */
        super((Void) null);
        map = new ConcurrentHashMap<>(initialCapacity);
        this.defaults = defaults;

        // storeFence 方法插入一个内存屏障,确保所有之前的写操作在此方法调用前完成。这对于确保多线程环境下的可见性和一致性至关重要
        UNSAFE.storeFence();
    }

2.4 Properties 加载配置资源文件

        首先,在加载资源文件的时候,我们需要思想一个问题?资源是以什么形式呈现的?

        第一种可以是 xxx.properties 文件的形成呈现;还可以是以 xxx.yaml 文件呈现的,也可以是以xxx.xml文件格式呈现的,还可以以 xxx.json 文件的形成呈现的。

        这些都是以文件的形成进行呈现,所以在进行资源文件读取的时候直接通过文件流进行一个读取与写入 FileInputStream/FileOutputStream 。这是一种资源的呈现

        配置资源文件,在程序启动已经运行了,处于运行期,可不可以从内存里面进行加载,如果要从内容里面进行加载在IO流里面通过ByteArrayInputStream/ ByteArrayOutputStream 进行资源加载;也可以通过网络传输过来的配置资源数据进行读取到 Properties

        所以总结一下,资源来源可以是从文件,内存,网络传输等等方式加载;所以在设计加载资源文件接口的时候,将参数设置 InputStream,而不是固定的 FileInputStream 文件流,或固定的内存流 ByteArrayInputStream。

        当然配置资源文件,其里面的内容都是字符串,当然除了支持字节流,同时也支持字符流。而且使用字符串读取配置文件更加方便。其一是配置文件本身就是字符串内容,其二是字符流提供一些高效的读取方式(比如整行整行的读取)

Java-01-源码篇-04集合-04-Properties,第8张

        然后再编写对应的解析器,比如PropertiesParser, YamlParser, XMLParser, JSONParser ; 最后将解析到内容追加到map里面;这是比较常规的处理方式。但是 Properties 类仅仅只支持处理两种类型的配置文件:properties 和 XML 这两种;

 2.4.1 load(inputStream) 

【load(inputStream)】加载配置文件,以字节流的形成进行加载

    /**
     * 加载配置文件,以字节流的形成进行加载
     * @since JDK 1.2
     */
    public synchronized void load(InputStream inStream) throws IOException {
        Objects.requireNonNull(inStream, "inStream parameter is null");
        load0(new LineReader(inStream)); // 转成 Properties 的静态内部类 LineReader 
    }

2.4.2 load(Reader)

【load(reader)】加载配置文件,以字符流的形成进行加载

    /**
     * 加载配置文件,以字符流的形成进行加载
     * @since JDK 1.6 
     */
    public synchronized void load(Reader reader) throws IOException {
        Objects.requireNonNull(reader, "reader parameter is null");
        load0(new LineReader(reader));
    }

2.4.3 load0(LineReader lr)

【load(lineReader)】真正加载配置文件业务方法

private void load0(LineReader lr) throws IOException {
        // 【第一步:初始化变量值】
        // 用于存储转换后的字符串。
        StringBuilder outBuffer = new StringBuilder();
        // 表示读取的行的长度
        int limit;
        // 键的长度
        int keyLen;
        // 值的起始位置
        int valueStart;
        // 表示是否存在键值分隔符(= 或 :)
        boolean hasSep;
        // 表示当前字符前是否有反斜杠
        boolean precedingBackslash;

        // 【第二步:读取行,并返回行的长度】
        while ((limit = lr.readLine()) >= 0) {
            keyLen = 0;
            valueStart = limit;
            hasSep = false;  
            precedingBackslash = false;

            // 【第三步:解析并分割键和值】
            while (keyLen < limit) {
                /** 获取键和值的分割符,通过从key 索引逐个遍历直到一行的末尾 */
                char c = lr.lineBuf[keyLen];
                // 获取第一个内容是 '=' ; ':' 则获取到key的分隔符,进行返回
                if ((c == '=' ||  c == ':') && !precedingBackslash) {
                    valueStart = keyLen + 1;
                    hasSep = true;
                    break;
                // 增加分隔符范围, ' ' 空字符串, '\t' 转义为 tab键, '\f' 一种古老的打印机打印下一页的标记 这三种情况也为key-value的分割范围
                } else if ((c == ' ' || c == '\t' ||  c == '\f') && !precedingBackslash) {
                    valueStart = keyLen + 1;
                    break;
                }
                if (c == '\') {
                    precedingBackslash = !precedingBackslash;
                } else {
                    precedingBackslash = false;
                }
                keyLen++;
            }
            
            // 分隔符不算,进行跳过,找到值真正的起始位置,
            while (valueStart < limit) {
                char c = lr.lineBuf[valueStart];
                if (c != ' ' && c != '\t' &&  c != '\f') {
                    if (!hasSep && (c == '=' ||  c == ':')) {
                        hasSep = true;
                    } else {
                        break;
                    }
                }
                valueStart++;
            }
            /**
             * loadConvert 方法讲解
             * 第一个参数:lr.lineBuf 这一行的全部内容,
             * 第二个参数:0,是从索引为0 开始读取
             * 第三个参数:索引结束位置
             * 第四个参数:是输出数据的缓存,如果是key,就是存储key的缓存,value就是存储value的缓存数据
             */
            // 【第四步】所以获取key 和value
            String key = loadConvert(lr.lineBuf, 0, keyLen, outBuffer);
            String value = loadConvert(lr.lineBuf, valueStart, limit - valueStart, outBuffer);
            put(key, value);
        }
    }

总结:

        Properties 的load方法仅仅只支持xxx.properties 这种格式的配置文件资源的加载与解析。

        其二,xxx.properties配置文件的分割符号可以:'=',':',' ', '\t', '\f'

2.4.4 load测试

Java-01-源码篇-04集合-04-Properties,第9张

Java-01-源码篇-04集合-04-Properties,第10张

源码如下:

# 以等于号进行分割 key-value
toast.datasource.type=com.alibaba.druid.pool.DruidDataSource
toast.datasource.driverClassName=com.alibaba.druid.pool.DruidDataSource
toast.datasource.username=toast
toast.datasource.password=toast
toast.datasource.metadata=希望大家多多支持与关注,文章有问题欢迎指导~~~
toast.blog.csdn=https://blog.csdn.net/qq_45281820

# 以 ":" 进行分割 key-value
toast.redis.timeout:50s
toast.redis.host:localhost
toast.redis.password:123456Redis
toast.redis.port:6379

# 以 tab键 进行key-value分割
toast.application   toast的项目
toast.debugger.level    ERROR
toast.debugger.logger.url   '/opt/log/toast/debugger.log'

# 以 ' ' 空字符串进行分割 key-value
toast.mybatis-plus.typeAliasesPackage com.toast.**.domain
toast.mybatis-plus.mapperLocations 'classpath*:mapper/**/*/Mapper.xml'

public class PropertiesLoadTest {

    public static void main(String[] args) throws IOException {
        Properties properties = new Properties();
        InputStream inputStream = new FileInputStream(new File("src/main/resources/toast-application.properties"));
        properties.load(inputStream);
        inputStream.close();
        System.out.println(properties);
    }
}

2.5 Properties 保存配置资源文件

2.5.1 save(out, comments)

【save(OutputStream, comments)】将配置信息以字节流的信息写出

    @Deprecated
    public void save(OutputStream out, String comments)  {
        try {
            store(out, comments);
        } catch (IOException e) {
        }
    }

2.5.2 store(OutputStream, comments)

【store】将配置信息以字节流的信息写入

    public void store(OutputStream out, String comments) throws IOException {
        store0(new BufferedWriter(new OutputStreamWriter(out, ISO_8859_1.INSTANCE)),
               comments,
               true);
    }

2.5.3  store0(BufferedWriter bw, comments, escUnicode)

【store0】将配置信息以字节流的信息写入

    private void store0(BufferedWriter bw, String comments, boolean escUnicode)
        throws IOException
    {
        // 如果注释不为空,则写入所有注释内容
        if (comments != null) {
            writeComments(bw, comments);
        }
        // 写入当前时间
        bw.write("#" + new Date().toString());
        bw.newLine(); // 写入一个分隔符 \n
        // 同步将 properties 信息全部写入指定字符输出流
        synchronized (this) {
            // 获取当前properties对象所有的配置信息,通过for循环逐行写入
            for (Map.Entry<Object, Object> e : entrySet()) {
                String key = (String)e.getKey();
                String val = (String)e.getValue();
                key = saveConvert(key, true, escUnicode);
                val = saveConvert(val, false, escUnicode);
                bw.write(key + "=" + val);
                bw.newLine();
            }
        }
        // 进行输出流刷新
        bw.flush();
    }

 2.5.4 测试保存资源配置文件

package com.toast.map;

import java.io.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

/**
 * @author toast
 * @time 2024/7/7
 * @remark
 */
public class StorePropertiesTest {
    public static void main(String[] args) {
        // 配置文件注释描述
        String comments = " I have a dream \r\n I have a dream   \u00ff";
        try (
                InputStream inputStream = new FileInputStream(new File("src/main/resources/toast-application.properties"));
                OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(new File("src/main/resources/toast-out-application.properties")), "UTF-8");
        ) {
            // 加载资源配置文件
            Properties properties = new Properties();
            properties.load(inputStream);
            properties().entrySet().forEach(prop -> properties.setProperty(prop.getKey(), prop.getValue()));
            // 将资源保存到toast-out-application.properties配置文件中
            properties.store(writer, comments);
        } catch (IOException e) {}

    }

    public static Map<String, String> properties() {
        Map properties = new HashMap();
        properties.put("properties.social.oauth.github", "github.com");
        properties.put("properties.social.oauth.QQ", "QQ123");
        properties.put("properties.social.oauth.gitee", "gitee.com");
        properties.put("properties.social.oauth.wechat", "WECHAT");
        properties.put("properties.social.oauth.facebook", "facebook.com");
        return properties;
    }
}

Java-01-源码篇-04集合-04-Properties,第11张

2.6 Properties 加载 XML 配置资源文件

2.6.1 loadFromXML(InputStream)

    public synchronized void loadFromXML(InputStream in)
        throws IOException, InvalidPropertiesFormatException {
        Objects.requireNonNull(in);
        PropertiesDefaultHandler handler = new PropertiesDefaultHandler();
        handler.load(this, in);
        in.close();
    }

        该类借助的是 PropertiesDefaultHandler 类来进行处理 XML 资源文件。而该的全限定名称如下:jdk.internal.util.xml.PropertiesDefaultHandler。java.base 模块下 其 JDK内部组件的工具类。该类以 SAX 的方式实现XML属性的加载与保存

Java-01-源码篇-04集合-04-Properties,第12张

2.6.2 测试 Properties 加载 XML 资源配置文件

Java-01-源码篇-04集合-04-Properties,第13张

 Java-01-源码篇-04集合-04-Properties,第14张

package com.toast.map;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

/**
 * @author toast
 * @time 2024/7/7
 * @remark
 */
public class LoadXMLPropertiesTest {
    public static void main(String[] args) {
        try (
            InputStream inputStream = new FileInputStream(new File("src/main/resources/toast-application.xml"));
        ) {
            Properties properties = new Properties();
            properties.loadFromXML(inputStream);
            System.out.println(properties);
        } catch (IOException e) {

        }
    }
}

Java-01-源码篇-04集合-04-Properties,第15张

2.7 Properties 保存 XML 配置资源文件

2.7.1 storeToXML(outputStream, comment)

        通过输出字节流进行保存 XML 资源配置文件,默认指定使用 UTF-8 字符集

    public void storeToXML(OutputStream os, String comment) throws IOException {
        storeToXML(os, comment, UTF_8.INSTANCE);
    }

 2.7.2 storeToXML(OutputStream, comment, encoding)

        通过字节输出流和指定指定字符集进行保存 XML 资源配置文件

    public void storeToXML(OutputStream os, String comment, String encoding) throws IOException {
        // 断言 输出流和字符集不能为空
        Objects.requireNonNull(os);
        Objects.requireNonNull(encoding);

        try {
            // 将字符集封装成 Charset
            Charset charset = Charset.forName(encoding);
            // 调用storeToXML 进行保存 XML 配置
            storeToXML(os, comment, charset);
        } catch (IllegalCharsetNameException | UnsupportedCharsetException e) {
            throw new UnsupportedEncodingException(encoding);
        }
    }

2.7.3 storeToXML(OutputStream, comment, Charset)

    public void storeToXML(OutputStream os, String comment, Charset charset)
        throws IOException {
        Objects.requireNonNull(os, "OutputStream");
        Objects.requireNonNull(charset, "Charset");
        PropertiesDefaultHandler handler = new PropertiesDefaultHandler();
        handler.store(this, os, comment, charset);
    }

         从代码中,可以得知也是借助 PropertiesDefaultHandler 类来实现 XML 配置保存,和load一样。

2.7.4 测试 Properties 保存 XML 配置文件

package com.toast.map;

import java.io.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

/**
 * @author toast
 * @time 2024/7/7
 * @remark
 */
public class StoreXMLPropertiesTest {

    public static void main(String[] args) {
        // 配置文件注释描述
        String comments = " I have a dream I have a dream";
        try (
                InputStream inputStream = new FileInputStream(new File("src/main/resources/toast-application.xml"));
                OutputStream writer = new FileOutputStream(new File("src/main/resources/toast-out-application.xml"));
        ) {
            // 加载资源配置文件
            Properties properties = new Properties();
            properties.loadFromXML(inputStream);
            properties().entrySet().forEach(prop -> properties.setProperty(prop.getKey(), prop.getValue()));
            // 将资源保存到toast-out-application.properties配置文件中
            properties.storeToXML(writer, comments);
        } catch (IOException e) {}

    }

    public static Map<String, String> properties() {
        Map properties = new HashMap();
        properties.put("properties.social.oauth.github", "github.com");
        properties.put("properties.social.oauth.QQ", "QQ123");
        properties.put("properties.social.oauth.gitee", "gitee.com");
        properties.put("properties.social.oauth.wechat", "WECHAT");
        properties.put("properties.social.oauth.facebook", "facebook.com");
        return properties;
    }
}

Java-01-源码篇-04集合-04-Properties,第16张

Java-01-源码篇-04集合-04-Properties,第17张

2.8 Properties 设置资源文件

        设置资源配置项

    /**
     * 设置配置项
     * @param key 
     * @param value
     */
    public synchronized Object setProperty(String key, String value) {
        return put(key, value);
    }

        这里需要说明一下,设置资源配置项的时候,为什么需要将put进行一层包装,而不直接调用put()方法。

        首先,Properties 是一个资源配置项,里面的资源描述都是字符串的形成进行显现的,而不是以二进制数据流等等方式进行呈现,所以进行一层包装保证配置资源数据格式统一,不会出现类转换异常。所以官网也强烈建议调用setProperties 而非 put()方法,调用put(); 如果类型不是String,在进行list遍历的时候会有类型强转异常。

2.8.1 测试类型强转异常

/**
 * @author toast
 * @time 2024/7/7
 * @remark
 */
public class SetPropertiesTest {
    public static void main(String[] args) {
        Properties properties = new Properties();
        properties.setProperty("name", "toast");
        properties.setProperty("age", "1000");
        properties.list(System.out);

        System.out.println("====================下面有 ClassCastException=======================");
        Properties classCastProperties = new Properties();
        classCastProperties.put("name", "toast");
        classCastProperties.put("age", 23);
        classCastProperties.list(System.out);
    }
}

Java-01-源码篇-04集合-04-Properties,第18张

        在遍历到第二行的就是有类型强转异常,尽管我第一行用的是put,但是类型都是String, 就不会,如果是其他类型就会

2.9 Properties 获取资源文件

2.9.1 getProperty(key, defaultValue)

        根据key获取value,如果为空,则返回指定的defaultValue

    /**
     * 根据key获取value
     * @param key key值
     * @param defaultValue 如果key获取为空,则返回指定的defaultValue
     */
    public String getProperty(String key, String defaultValue) {
        String val = getProperty(key);
        return (val == null) ? defaultValue : val;
    }

2.9.2 getProperty(key)

        根据key获取value, 如果value 为 null; 但 defaults 不为空,则在defaults再获取一遍,有就返回,没有还是返回null 

    /**
     * 根据key获取value
     * @param key key值
     */
    public String getProperty(String key) {
        // 获取map的value
        Object oval = map.get(key);
        // 如果value 不为 String 则值为null
        String sval = (oval instanceof String) ? (String)oval : null;
        Properties defaults;
        // 如果值为空,并且还有默认的defaults 则从defaults在获取一遍,有就返回,没有还是返回null
        return ((sval == null) && ((defaults = this.defaults) != null)) ? defaults.getProperty(key) : sval;
    }

2.9.3 获取Properties全部配置文件的keys

    /**
     * 获取properties 全部keys值,也就是names
     */
    public Set<String> stringPropertyNames() {
        Map<String, String> h = new HashMap<>();
        enumerateStringProperties(h);
        return Collections.unmodifiableSet(h.keySet());
    }

2.9.4 list(PrintStream) 遍历Properties 配置文件资源

    public void list(PrintStream out) {
        out.println("-- listing properties --");
        Map<String, Object> h = new HashMap<>();
        enumerate(h);
        for (Map.Entry<String, Object> e : h.entrySet()) {
            String key = e.getKey();
            String val = (String)e.getValue();
            if (val.length() > 40) {
                val = val.substring(0, 37) + "...";
            }
            out.println(key + "=" + val);
        }
    }

2.9.5 list(PrintWriter) 遍历Properties 配置文件资源

    public void list(PrintWriter out) {
        out.println("-- listing properties --");
        Map<String, Object> h = new HashMap<>();
        enumerate(h);
        for (Map.Entry<String, Object> e : h.entrySet()) {
            String key = e.getKey();
            String val = (String)e.getValue();
            if (val.length() > 40) {
                val = val.substring(0, 37) + "...";
            }
            out.println(key + "=" + val);
        }
    }

2.9.6 enumerate(map) &  enumerateStringProperties 遍历

        进行遍历,并将元素追加到 h Map集合中

    private void enumerate(Map<String, Object> h) {
        if (defaults != null) {
            defaults.enumerate(h);
        }
        for (Map.Entry<Object, Object> e : entrySet()) {
            String key = (String)e.getKey();
            h.put(key, e.getValue());
        }
    }

        只遍历String类型的数据、并将符合的元素添加到 h Map集合中 

    private void enumerateStringProperties(Map<String, String> h) {
        if (defaults != null) {
            defaults.enumerateStringProperties(h);
        }
        for (Map.Entry<Object, Object> e : entrySet()) {
            Object k = e.getKey();
            Object v = e.getValue();
            if (k instanceof String && v instanceof String) {
                h.put((String) k, (String) v);
            }
        }
    }

2.10 其他方法都是 ConcurrentHashMap 用于替换 HashTable 重写相关Map集合方法

Java-01-源码篇-04集合-04-Properties,第19张

         

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明原文出处。如若内容造成侵权/违法违规/事实不符,请联系SD编程学习网:675289112@qq.com进行投诉反馈,一经查实,立即删除!