Java集合—Properties的源码深度解析以及应用介绍

  本文基于JDK1.8详细介绍了Properties集合的构造方法、遍历方法以及其他API方法的底层源码实现,最后给出了Properties的使用案例。

1 Properties的概述

public class Properties
  extends Hashtable<Object,Object>

  Properties,来自于JDK1.0的集合类,继承了Hashtable,它是线程安全的。
  该类没有泛型,因为它的元素类型是固定的:key和value都是只能是String类型,尽管你可以使用put方法存储其它类型的key-value,但是不建议这么做。
  向Properties集合当中存放的key-value 键值对,称之为集合的属性,所以这个集合,也称之为属性表集合
  最重要的是,该集合能够和IO流进行结合,可将数据保存在流中或从流中加载,也能够和外部的.properties配置文件或者是.xml文件进行交互,读取文件中的配置信息,使得用户可以将一些配置信息从代码中移除,实现了代码和配置信息的解耦,避免硬编码。

2 Properties的源码解析

2.1 主要类属性

  Properties继承了Hashtable,自然继承了它的属性,比如table、threshold、count等属性。因此,在了解Properties之前可以了解Hashtable:Java集合—Hashtable的源码深度解析以及应用介绍
  它也有自己的一个属性 defaults。实际上该属性的作用就是在属性表本身中没找到对应的属性时,再从defaults(如果存在)中寻找,这里的defaults用于存储属性的默认值,因此这个属性的值是可有可无的(根据你的需求)。

/**
 * 一个内部属性列表,其中包含此属性列表中未找到的任何键的默认值。
 * 实际上该属性的作用就是在属性表本身中没找到对应的属性时,在从defaults(如果存在)中寻找,这里的defaults用于存储属性的默认值
 */
protected Properties defaults;

2.2 构造器

2.2.1 Properties()

public Properties()

  创建一个没有默认值的空的属性表集合。

public Properties() {
    //内部调用另一个构造器,传递的defaults为null
    this(null);
}

2.2.2 Properties(Properties defaults)

public Properties(Properties defaults)

  创建一个带有指定默认值的空属性列表。默认值也可以指定为null。

public Properties(Properties defaults) {
    this.defaults = defaults;
}

2.3 遍历的方法

  Properties继承了Hashtable的全部遍历方法,包括keySet()、entrySet()、values()、keys()、elements()等五个方法,同时还具有自己的特有的两个遍历方法:propertyNames和stringPropertyNames。
  下面主要来看它自己的遍历方法,父类的便利方法在Hashtable一文中有详细讲解!

2.3.1 propertyNames方法

public Enumeration< ? > propertyNames()

  返回属性表中所有键的枚举,包括默认属性表中的键(如果存在)。JDK1.0的方法。
  如果Properties中存在非String类型的key,那么将会报错:ClassCastException。

/**
 * @return 返回属性表中所有键的枚举,包括默认属性表中的键。
 */
public Enumeration<?> propertyNames() {
    //这里新建一个hashtable命名为h
    Hashtable<String, Object> h = new Hashtable<>();
    //这个方法用于向h中存放遍历到的key-value
    enumerate(h);
    //通过hashtable的keys方法返回key的枚举
    return h.keys();
}

/**
 * 用于向h中存放遍历到的key-value,如果存在非String类型的key,那么将会报错:ClassCastException
 *
 * @param h
 */
private synchronized void enumerate(Hashtable<String, Object> h) {
    //这一步表明,如果内部的defaults不为null的话,那么defaults将会递归调用这个方法……直到defaults为null,然后开始下面的代码
    if (defaults != null) {
        defaults.enumerate(h);
    }
    //到这一步,说明从外到内遍历到了最底层的defaults(为null),那么开始从内到外遍历每一层defaults的keys枚举,并且加入到这个hashtable中!
    for (Enumeration<?> e = keys(); e.hasMoreElements(); ) {
        //获取key 这里强行转换为String,我们能够知道,如你你不存储String类型的key,那么执行到这里就会报错
        String key = (String) e.nextElement();
        //向hashtable存入key,value  如果value为null,那么也会报错了。
        h.put(key, get(key));
    }
}

2.3.2 stringPropertyNames方法

public Set< String > stringPropertyNames()

  返回此属性表中的键集,其中该键及其对应值是字符串,如果在主属性列表中未找到同名的键,则还包括默认属性列表中不同的键。
  JDK1.6新加的方法。相比于propertyNames方法,更加容易对结果进行操作,并且相对友好,对于非String类型的key和value,在propertyNames方法中会直接报错,在该方法中将会被忽略而正常返回。

public Set<String> stringPropertyNames() {
    //这里新建一个hashtable命名为h
    Hashtable<String, String> h = new Hashtable<>();
    //这个方法用于向h中存放遍历到的key-value
    enumerateStringProperties(h);
    //通过hashtable的keySet方法返回key的set集合
    return h.keySet();
}

/**
 * 用于向h中存放遍历到的String类型的key-value,存在非String类型的key-value将会忽略
 *
 * @param h
 */
private synchronized void enumerateStringProperties(Hashtable<String, String> h) {
    //这一步表明,如果内部的defaults不为null的话,那么defaults将会递归调用这个方法……直到defaults为null,然后开始下面的代码
    if (defaults != null) {
        defaults.enumerateStringProperties(h);
    }
    //到这一步,说明从外到内遍历到了最底层的defaults(为null),那么开始从内到外遍历每一层defaults的keys枚举,并且加入到这个hashtable中!
    for (Enumeration<?> e = keys(); e.hasMoreElements(); ) {
        //获取key value
        Object k = e.nextElement();
        Object v = get(k);
        //如果k和v 不都是String类型,那么将会被忽略,而不是报错
        if (k instanceof String && v instanceof String) {
            //如果k和v 都是String类型,那么将会加入h
            h.put((String) k, (String) v);
        }
    }
}

2.3.3 总结

  我们遍历Properties时,这两个方法它会额外遍历defaults中的属性,而Hashtable的便利方法则不会,因此需要慎重选择!
  同时如果需要遍历defaults中的属性并返回,那么只能使用propertyNames和stringPropertyNames方法,它的区别是返回值类型不一样,第一个是Enumeration枚举,第二个是set集合,set集合更加方便操作,支持iterator、Iterator.remove、Set.remove、removeAll、 retainAll、和 clear 操作。即此set支持元素移除,可从映射中移除相应的映射关系,但它不支持 add 或 addAll 操作。
  并且如果遇到非String类型的key,propertyNames方法将会抛出异常;而stringPropertyNames对于不全是String类型的key-value会忽略而不是抛出异常。

2.4 从外部文件读取的方法

  Properties集合最重要的功能就是从外部配置文件中获取配置信息,Properties可以从.properties文件和.XML文件中读取信息,那么自然Properties提供了相应的方法!
  .properties文件中的数据必须是键值对形式,可以使用“=”、“:”、“ ”、“\t”、“\f”等符号分隔。注意这里的\t表示制表符,\f表示换页符。这里的“空格”可以是\u00A0(不间断空格)、\u0020(半角/英文空格)、\u3000(全角/中文空格)。同时“#”、“!”开头的一行被算作注释不会解析。

读取properties文件:

public void load(InputStream inStream)

  从字节输入流中读取属性列表(键和元素对)。

public void load(Reader reader)

  按简单的面向行的格式从输入字符流中读取属性列表(键和元素对)。

读取xml文件:

public void loadFromXML(InputStream in)

  将指定字节输入流中由 XML 文档所表示的所有属性加载到此属性表中。

2.4.1 load方法

  简单看看load(InputStream inStream)方法的源码,这些方法的原理都差不多,都是从IO流中读取数据而已。

public synchronized void load(InputStream inStream) throws IOException {
    //调用loca0方法,传入LineReader对象,顾名思义,该对象能够一行一行的读取数据
    load0(new LineReader(inStream));
}

/**
 * 从LineReader中读取数据到propeties中
 *
 * @param lr
 * @throws IOException
 */
private void load0(LineReader lr) throws IOException {
    char[] convtBuf = new char[1024];
    int limit;
    int keyLen;
    int valueStart;
    char c;
    boolean hasSep;
    boolean precedingBackslash;
    /*每次读取一行,循环读取
     * 如果有数据,那么linit是大于0的*/
    while ((limit = lr.readLine()) >= 0) {
        c = 0;
        keyLen = 0;
        valueStart = limit;
        hasSep = false;

        //System.out.println("line=<" + new String(lineBuf, 0, limit) + ">");
        precedingBackslash = false;
        while (keyLen < limit) {
            c = lr.lineBuf[keyLen];
            //检测分隔符
            // =  :  可以用作分隔符
            if ((c == '=' || c == ':') && !precedingBackslash) {
                valueStart = keyLen + 1;
                hasSep = true;
                break;
                // 空格  \t   \f 也可以用作分隔符
            } else if ((c == ' ' || c == '\t' || c == '\f') && !precedingBackslash) {
                valueStart = keyLen + 1;
                break;
            }
            //  \\将会被解析成\
            if (c == '\\') {
                precedingBackslash = !precedingBackslash;
            } else {
                precedingBackslash = false;
            }
            keyLen++;
        }
        while (valueStart < limit) {
            c = lr.lineBuf[valueStart];
            if (c != ' ' && c != '\t' && c != '\f') {
                if (!hasSep && (c == '=' || c == ':')) {
                    hasSep = true;
                } else {
                    break;
                }
            }
            valueStart++;
        }
        //获取解析到的key和value
        String key = loadConvert(lr.lineBuf, 0, keyLen, convtBuf);
        String value = loadConvert(lr.lineBuf, valueStart, limit - valueStart, convtBuf);
        //调用put方法存入集合中
        put(key, value);
    }
}

2.5 输出到外部文件的方法

输出到properties文件中:

public void store(OutputStream out,String comments)

  写入输出字节流。
  Out:指定一个输出流;comments:属性列表的描述,如果没有所需的注释,则可以为null。。

public void store(Writer writer,String comments)

  写入输出字符流。

输出到xml文件中:

public void storeToXML(OutputStream os,String comment)

  发出一个表示此表中包含的所有属性的 XML 文档。

public void storeToXML(OutputStream os,String comment,String encoding)

  使用指定的编码发出一个表示此表中包含的所有属性的 XML 文档。

2.6 其他方法

public synchronized Object setProperty(String key, String value)

  添加属性,要求属性的键和值使用字符串。内部调用 Hashtable 的方法 put,返回值是 Hashtable 调用 put 的结果。这里可知该方法存放的键值对是存放在table属性中而不是defaults属性中!
  Properties集合存放属性应该使用该方法而不是put。

public synchronized Object setProperty(String key, String value) {
    return put(key, value);
}

public String getProperty(String key)

  用指定的键在此属性列表中搜索属性。如果在此属性列表中未找到该键,则接着递归检查默认属性列表及其默认值。如果未找到属性,则此方法返回 null。
  Properties集合获取属性值应该使用该方法而不是get。

public String getProperty(String key) {
    //使用hashtable的方法获取value,注意是Object类型
    Object oval = super.get(key);
    //如果oval属于String类型,那么强转为String赋值给sval,否则sval=null
    String sval = (oval instanceof String) ? (String) oval : null;
    //如果sval=null并且defaults不为空,那么说明在table中没有找到该键值对,那么从defaults中查找
    //这里的defaults还是调用的getProperty方法,可以看出来是递归查找
    return ((sval == null) && (defaults != null)) ? defaults.getProperty(key) : sval;
}

3 读取文件案例演示

  我们来简单演示如何启动时读取.properties文件的配置信息,主要分为两步:

  1. 获取文件的IO流;
  2. 从IO流中读取数据;
public class ReadFileByClassLoader {
    /**
     * 定义空的proerties集合为类变量,随着类的加载而加载,并且该类的所有的块和方法都能访问
     */
    private static Properties properties = new Properties();

    /**
     * 将使用加载器读取类路径配置文件的方法定义在静态块中,使得类创建的时候就能读取配置文件的信息
     */
    static {
        try {
            // 获得字节码文件对象,有三种方法获取!
            //Class<? extends ReadFileByClassLoader> class1 = ReadFileByClassLoader.class;
            //Class<?> class1 = Class.forName("collection.map.ReadFileByClassLoader");  //2
            //Class<? extends ReadFileByClassLoader> aClass = new ReadFileByClassLoader().getClass();  //3

            /*方法1 ==> 获得字节码文件对象,然后获得类的加载器,调用类加载器的方法 获取输入流*/
            //getResourceAsStream()方法的参数需要包路径从resources下一级的包开始+properties文件名+.后缀
            //这里的student.properties文件就存在resources包下面,因此直接写名字
            //InputStream resourceAsStream = ReadFileByClassLoader.class.getClassLoader().getResourceAsStream("student.properties");

            /*方法2 ==> 也可以直接使用字节码文件对象的getResourceAsStream方法获取输入流*/
            //注意这里前面要加/
            //InputStream resourceAsStream = ReadFileByClassLoader.class.getResourceAsStream("/student.properties");

            /*方法3 ==> 使用类加载器的getSystemResourceAsStream方法获取输入流*/
            //InputStream resourceAsStream = ClassLoader.getSystemResourceAsStream("student.properties");

            /*方法4 ==> 直接使用输入流读取文件字节*/
            //new File()的参数按格式需要包路径从src包开始+properties文件名+.后缀
            //这里的student.properties文件存放在resources包下面
            InputStream resourceAsStream = new BufferedInputStream(new FileInputStream(new File("src/main/resources/student.properties")));
            //调用properties的load加载输入流
            properties.load(resourceAsStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws IOException {
        // 启动即可直接获取配置文件的信息
        System.out.println(properties);
    }
}

  主要有4种获取输入流的方法,要注意他们所需要传递的路径不同,这里本人的项目是一个idea创建普通maven项目,student.properties文件就放在resources的目录下面,不同的项目结构以及配置文件存放的位置不同,都将可能导致加载的路径不一样。
  这里的student.properties配置文件内容如下:

! !作为注释
# #作为注释
# 等号分隔
name=张三
# :分隔
age:22
# 空格分隔
score 22.1
a           i
# \t分隔
b  xx
#  \\解析成\
c=x\\

  输出如下(注意使用UTF-8编码,否则可能乱码):

{b=xx, a=i, age=22, name=张三, score=22.1, c=x\}

如果有什么不懂或者需要交流,可以留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

猜你喜欢

转载自blog.csdn.net/weixin_43767015/article/details/106712135