XML与设计模式
一、XML
1.1 概述
什么是XML?
- XML是一种可扩展标记语言(eXtensible Markup Language)的缩写,它是一种数据表示格式,可以描述非常复杂的数据结构,常用于传输和存储数据。
- 特点
- 一是纯文本,默认使用UTF-8编码,二是可嵌套。
- 如果把XML内容存为文件,那么他就是一个XML文件。
- 作用
- XML内容经常被当成消息进行网络传输
- 作为配置文件用于存储系统的信息。
1.2 XML的创建、语法规则
XML就是创建一个XML类型的文件,要求文件的后缀必须使用xml,如hello_world.xml
语法规则
- XML文件的后缀名为:xml
- 文档声明必须是第一行
<?xml version="1.0" encoding="UTF-8" ?> <!-- 注释内容 version:XML默认的版本号码、该属性必须存在 encoding:本XML文件的编码 -->
XML的标签(元素)规则
- 标签是由一对尖括号和合法标识符组成:,必须存在一个根标签,有且只能有一个。
- 标签必须成对出现,有开始,有结束:
- 特殊的标签不可以成对,但是必须有结束标记,如:
- 标签中可以定义属性,属性和标签名空格隔开,属性值必须用引号引起来****
- 标签需要正确的嵌套
XML的其他组成
XML文件中可以注释信息:
XML文件中可以存在以下特殊字符
- XML文件中可以存在CDATA区:<![CDATA[...内容...]]>
1.3 XML文档约束
为什么需要文档约束?
- 由于XML文件可以自定义标签,导致XML文件可以随意定义,程序在解析的时候可以出现问题。
- 所以使用文档约束来限定XML文件中的标签以及属性怎么写,以此强制约束程序员必须按照文档约束的规定来编写XML文件。
1.3.1 DTD约束[了解]
XML文档约束的作用:
- 约束XML文件的编写
- 不可以约束具体的数据类型
需求:利用DTD文档约束,约束一个XML文件的编写。
分析:
- 编写DTD约束文档,后缀必须是.dtd
在需要编写的XML文件中导入该DTD约束文档
按照约束的规定编写XML文件的内容
<?xml version="1.0" encoding="UTF-8" ?>
<!-- 导入dtd编写的约束 -->
<!DOCTYPE 书架 SYSTEM "data.dtd">
<书架>
<书>
<书名>精通JavaSE加强</书名>
<作者>dlei</作者>
<!-- 由此可知dtd无法对数据类型约束 -->
<售价>很贵</售价>
</书>
<书>
<书名></书名>
<作者></作者>
<售价></售价>
</书>
<书>
<书名></书名>
<作者></作者>
<售价></售价>
</书>
</书架>
<!ELEMENT 书架 (书+)>
<!ELEMENT 书 (书名,作者,售价)>
<!ELEMENT 书名 (#PCDATA)>
<!ELEMENT 作者 (#PCDATA)>
<!ELEMENT 售价 (#PCDATA)>
1.3.2 Schema约束[了解]
文档约束-schema
- schema可以约束具体的数据类型,约束能力上更强大。
- schema本身也是一个xml文件,本身也收到其他约束文件的要求,所以编写的更加严谨。
优点
- 可以约束XML文件的标签内容格式,以及具体的数据类型
- 本身也是XML文件,格式更严谨
需求:利用schema文档约束,约束一个XML文件的编写
分析:
- 编写schema约束文档,后缀必须是.xsd,具体的形式到代码中观看。
- 在需要编写的XML文件中导入该schema约束文档
- 按照约束内容编写XML文件的标签
<?xml version="1.0" encoding="UTF-8" ?>
<书架 xmlns="http://www.baidu.cn"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.baidu.cn data.xsd">
<!-- xmlns="http://www.baidu.cn" 基本位置
xsi:schemaLocation="http://www.baidu.cn books02.xsd" 具体的位置 -->
<书>
<书名>神雕侠侣</书名>
<作者>金庸</作者>
<售价>399.9</售价>
</书>
<书>
<书名>神雕侠侣</书名>
<作者>金庸</作者>
<售价>19.5</售价>
</书>
</书架>
<?xml version="1.0" encoding="UTF-8" ?>
<schema xmlns="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.baidu.cn"
elementFormDefault="qualified" >
<!-- targetNamespace:申明约束文档的地址(命名空间)-->
<element name='书架'>
<!-- 写子元素 -->
<complexType>
<!-- maxOccurs='unbounded': 书架下的子元素可以有任意多个!-->
<sequence maxOccurs='unbounded'>
<element name='书'>
<!-- 写子元素 -->
<complexType>
<sequence>
<element name='书名' type='string'/>
<element name='作者' type='string'/>
<element name='售价' type='double'/>
</sequence>
</complexType>
</element>
</sequence>
</complexType>
</element>
</schema>
二、XML解析技术
XML的数据的作用是什么,最终需要怎么处理?
- 存储数据、做配置信息、进行数据传输。
- 最终需要被程序进行读取,解析里面的信息。
2.1 XML解析技术概述
什么是XML解析?
- 使用程序读取XML中的数据
两种解析方式:
- SAX解析(一行行解析xml文件)
- DOM解析(直接将xml文件放进内存使用树结构来解析)
2.1.1 DOM常见的解析工具
名称 | 说明 |
---|---|
JAXP | SUN公司提供的一套XML的解析的API |
JDOM | JDOM是一个开源项目,他基于树形结构,利用纯JAVA的技术对XML文档实现解析、生成、序列化以及多种操作。 |
dom4j | 是JDOM的升级品,用来读写XML文件的。具有性能优异、功能强大和极其易使用的特点,它的性能超过sun公司官方的dom技术,同时他也是一个开源代码的软件,Hibernate也用它来读写配置文件 |
jsoup | 功能强大DOM方式的XML解析开发包,尤其对HTML解析更加方便 |
2.1.2 DOM解析文档对象模型
DOM解析常用技术框架:Dom4J
Document对象:整个xml文档
Element对象:标签
Attribute对象:属性
Text对象:文本内容
2.2 Dom4J解析XML文件
需求:使用Dom4j解析出xml文件
分析:
- 下载Dom4j框架,官网下载。
- 在项目中创建文件夹:lib用于存放核心依赖包
- 将dom4j-2.1.1.jar文件复制到lib文件夹
- 在jar文件上右击选择Add asLibary -> 点击OK
- 在类中导包使用
2.2.1 使用Dom4j解析得到Document对象
SAXReader类
构造器/方法 | 说明 |
---|---|
public SAXReader() | 创建Dom4J的解析器对象 |
Document read(String url) | 加载XML文件成为Document对象 |
Domcument类
方法名 | 说明 |
---|---|
Element getRootElement() | 获得根元素对象 |
public static void main(String[] args) throws Exception {
//先创建dom4j解析器对象
SAXReader saxReader = new SAXReader();
//使用解析器的read方法加载xml文件为Document对象
//getResourceAsStream中的/是直接去src下寻找的文件,就算你的项目名称改了,也不影响寻找文件
Document read = saxReader.read(createXML.class.getResourceAsStream("/Contacts.xml"));
//得到Document对象的根节点对象,并打印名称是否正确
Element rootElement = read.getRootElement();
System.out.println(rootElement.getName());
}
<?xml version="1.0" encoding="UTF-8"?>
<contactList>
<contact id="1" vip="true">
<name> 孙悟空 </name>
<gender>男</gender>
<email>wukong@baidu.cn</email>
</contact>
<contact id="2" vip="false">
<name>唐僧</name>
<gender>男</gender>
<email>tangtang@baidu.cn</email>
</contact>
<contact id="3" vip="false">
<name>猪八戒</name>
<gender>男</gender>
<email>zhuzhu@baidu.cn</email>
</contact>
<contact id="4" vip="false">
<name>白骨精</name>
<gender>女</gender>
<email>baibai@baidu.cn</email>
</contact>
<user>
</user>
</contactList>
2.3 Dom4J解析XML文件中的各种节点
首先必须得到文档对象Document,从中获取元素对象和内容
Dom4J解析XML的元素、属性、文本
方法名 | 说明 |
---|---|
List elements() | 得到当前元素下所有子元素 |
List elements(String name) | 得到当前元素下指定名字的子元素返回集合 |
Element element(String name) | 得到当前元素下指定名字的子元素,如果很多名字相同的则返回第一个 |
String getName() | 得到元素名字 |
String attributeValue(String name) | 通过属性名直接得到属性值 |
String elementText(子元素名) | 得到指定名称的子元素的文本 |
String getText() | 得到文本 |
public static void main(String[] args) throws Exception {
//先创建dom4j解析器对象
SAXReader saxReader = new SAXReader();
//使用解析器的read方法加载xml文件为Document对象
//getResourceAsStream中的/是直接去src下寻找的文件,就算你的项目名称改了,也不影响寻找文件
Document read = saxReader.read(createXML.class.getResourceAsStream("/Contacts.xml"));
//得到Document对象的根节点,并打印名称是否正确
Element root = read.getRootElement();
System.out.println(root.getName());
//获取根节点下的所有子元素(获取一级子元素)
// List<Element> elements = root.elements();
//获取根节点下指定的一级子元素
List<Element> elements = root.elements("contact");
for (Element element : elements) {
System.out.println(element.getName());
}
//获取根节点下指定的一级子元素对象,默认为第一个
Element element = root.element("contact");
//获得该元素的指定属性
Attribute id = element.attribute("id");
//获得该属性的值
System.out.println(id.getText());//1
//直接通过根节点获取指定的子元素的属性值,使用attributeValue方法
String id1 = root.element("contact").attributeValue("id");
System.out.println(id1);//1
//获得该节点下的所有一级子元素
List<Element> elements1 = element.elements();
for (Element element1 : elements1) {
System.out.println(element1.getName());
//name
//gender
//email
}
//获取该节点下指定的一级子元素,默认第一个
Element name = element.element("name");
//打印该子元素的值
System.out.println(name.getText());// 孙悟空
//打印子元素的值,并去掉前后空格
System.out.println(name.getTextTrim());//孙悟空
}
<?xml version="1.0" encoding="UTF-8"?>
<contactList>
<contact id="1" vip="true">
<name> 孙悟空 </name>
<gender>男</gender>
<email>wukong@baidu.cn</email>
</contact>
<contact id="2" vip="false">
<name>唐僧</name>
<gender>男</gender>
<email>tangtang@baidu.cn</email>
</contact>
<contact id="3" vip="false">
<name>猪八戒</name>
<gender>男</gender>
<email>zhuzhu@baidu.cn</email>
</contact>
<contact id="4" vip="false">
<name>白骨精</name>
<gender>女</gender>
<email>baibai@baidu.cn</email>
</contact>
<user>
</user>
</contactList>
2.4 Dom4J解析XML文件-案例实战
通常数据会封装成Java的对象,如单个对象,或者集合对象形式
利用Dmo4J知识,将Contact.xml文件中的联系人数据封装成List集合,其中每一个元素都是实体类Contact。打印输出List中的每一个元素
private static List<Contact> list = new ArrayList<>();
public static void main(String[] args) throws Exception {
//创建解析器对象,加载xml文件为Document对象,使用getResourceAsStream方法需要加/代表src目录
SAXReader saxReader = new SAXReader();
Document read = saxReader.read(createXML.class.getResourceAsStream("/Contacts.xml"));
//获得Document对象的根节点(根节点只有一个)
Element root = read.getRootElement();
//获得根节点下所有的contact一级节点
List<Element> contact = root.elements("contact");
//遍历一级节点
for (Element element : contact) {
//创建一个对象用来存储遍历的值
Contact contact1 = new Contact();
//遍历每一个节点上的相关标签值和属性
contact1.setName(element.element("name").getTextTrim());
contact1.setSex(element.element("gender").getTextTrim().charAt(0));
contact1.setEmail(element.element("email").getTextTrim());
contact1.setId(Integer.valueOf(element.attributeValue("id")));
contact1.setVip(Boolean.valueOf(element.attributeValue("vip")));
//添加到list集合中
list.add(contact1);
}
//遍历集合
for (Contact contact1 : list) {
System.out.println(contact1);
}
//Contact{name='孙悟空', sex=男, email='wukong@baidu.cn', id=1, vip=true}
//Contact{name='唐僧', sex=男, email='tangtang@baidu.cn', id=2, vip=false}
//Contact{name='猪八戒', sex=男, email='zhuzhu@baidu.cn', id=3, vip=false}
//Contact{name='白骨精', sex=女, email='baibai@baidu.cn', id=4, vip=false}
}
public class Contact {
private String name ;
private char sex;
private String email;
private int id;
private boolean vip;
//省略有参和无参构造、getter和setter方法、toString重写方法。
}
三、XML检索技术:XPath
我们使用Dom4j进行文件的全部解析,然后在搜寻数据比较繁琐,那么我们有没有更简单的方法来检索某个信息呢?
- XPath技术更加适合做信息检索
3.1 XPath
什么是XPath?
- XPath在解析XML文档方面提供了独树一帜的路径思想,更加优雅,高效。
- XPath使用路径表达式来定位XML文档中的元素节点或属性节点。
注意:XPath技术也是依赖于Dom4j技术来检索
例如:
- /元素/子元素/孙子元素
- //子元素/孙子元素
方法名 | 说明 |
---|---|
Node selectSingleNode(“表达式”) | 获取符合表达式的唯一元素 |
List selectNodes(“表达式”) | 获取符合表达式的元素集合 |
3.2 XPath四大检索方案
绝对路径
采用绝对路径获取从根节点开始逐层的查找/contactList/contact/name节点列表并打印信息
方法名 说明 | /根元素/子元素/孙元素 | 从根元素开始,逐级向下查找,不能跨级 |
相对路径
先得到根节点contactList
再采用相对路径获取下一级contact节点的name子节点并打印信息
方法名 说明 | ./子元素/孙元素 | 从当前元素开始,一级一级向下查找,不能跨级 |
全文检索
直接全文搜索所有的name元素并打印
方法名 说明 | //contact | 找contact元素,无论在哪里 |
| //contact/name | 全文查找contact,name一定是contact的子节点 |
| //contact//name | 全文查找contact,name只要是contact的子孙元素都可以找到 |属性查找
全文搜索属性,或者带属性的元素
方法名 说明 | //@属性名 | 查找属性对象,无论是哪个元素,只要有这个属性即可 |
| //元素[@属性名] | 查找元素对象,全文搜索指定元素名和属性名 |
| //元素[@属性名=值] | 查找元素对象,全文搜索指定元素名和属性名,并且属性值相等 |
3.3 案例
需求:使用Dmo4j把一个XML文件的数据进行解析
分析:
- 导入jar包(Dmo4j和jaxen-1.1.2.jar),Xpath技术依赖于Dmo4j技术
- 通过dom4j的SAXReader获取Document对象
- 利用XPath提供的API,结合XPath的语法完成选取XML文档元素节点进行解析操作。
public class Demo1 {
//绝对路径检索
@Test
public void absolutePath() throws Exception {
//创建Dmo4j解析器
SAXReader s1 = new SAXReader();
//加载xml文件为Document对象
Document document = s1.read(Demo1.class.getResourceAsStream("/Contacts.xml"));
//使用绝对路径检索指定路径的标签
List<Node> node1 = document.selectNodes("/contactList/contact/name");
for (Node node : node1) {
//遍历打印检索到的name标签的文本内容,
// 检索不到沙和尚是因为沙和尚的路径不是这个绝对路径
System.out.println(node.getText());
// 孙悟空
//唐僧
//猪八戒
//白骨精
}
}
//相对路径检索
@Test
public void relativePaths() throws Exception {
//创建Dmo4j解析器
SAXReader s2 = new SAXReader();
//加载xml文件为Document对象
Document document = s2.read(Demo1.class.getResourceAsStream("/Contacts.xml"));
//使用相对路径检索节点信息
//需要先得到根节点
Element root = document.getRootElement();
//根据根节点相对路径找节点信息
List<Node> node2 = root.selectNodes("./contact/name");
//遍历得到的节点然后打印值
for (Node node : node2) {
System.out.println(node.getText());
// 孙悟空
//唐僧
//猪八戒
//白骨精
}
}
//全文检索
@Test
public void allRetrieve() throws Exception {
//创建Dmo4j解析器
SAXReader s3 = new SAXReader();
//加载xml文件为Document对象
Document document = s3.read(Demo1.class.getResourceAsStream("/Contacts.xml"));
//全文检索name标签,可以检索到沙和尚,因为沙和尚也属于name标签
List<Node> node3 = document.selectNodes("//name");
for (Node node : node3) {
System.out.println(node.getText());
// 孙悟空
//唐僧
//猪八戒
//白骨精
// 沙和尚
}
//全文检索contact标签,且email属于contact的子节点的email标签
//因为沙和尚没有email标签,所以检索不到
List<Node> node4 = document.selectNodes("//contact/email");
for (Node node : node4) {
System.out.println(node.getText());
//wukong@baidu.cn
//tangtang@baidu.cn
//zhuzhu@baidu.cn
//baibai@baidu.cn
}
//全文检索user标签,在user标签下再去寻找name标签,name属于子孙类都可以
//例如沙和尚的标签name属于user的孙子类
//selectSingleNode方法只会搜索一个,如果搜索到很多也只会默认为第一个节点
Node node5 = document.selectSingleNode("//user//name");
System.out.println(node5.getText());
// 沙和尚
}
//属性检索
@Test
public void attributesRetrieve() throws Exception {
//创建dmo4j解析器
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(Demo1.class.getResourceAsStream("/Contacts.xml"));
//使用//@属性名 可以定位属性对象
List<Node> node1 = document.selectNodes("//@id");
for (Node node : node1) {
System.out.println(node.getText());
//1
//2
//3
//4
}
//使用//元素[@属性名] 可以检索该元素中带有该属性的节点对象
List<Node> node2 = document.selectNodes("//contact[@id]");
for (Node node : node2) {
System.out.println(node.getName());
//contact
//contact
//contact
//contact
}
//使用//元素[@属性 = 值]可以检索该元素中带有该属性,且属性值为指定的节点对象
List<Node> node3 = document.selectNodes("//contact[@id = 4]");
for (Node node : node3) {
//因为一个node(节点)也是一个element(标签),所以直接强转
Element element = (Element) node;
//查看该节点下的name文本是不是我们指定id=4的name值
System.out.println(element.element("name").getText());
//白骨精
}
}
}
<?xml version="1.0" encoding="UTF-8"?>
<contactList>
<contact id="1" vip="true">
<name> 孙悟空 </name>
<gender>男</gender>
<email>wukong@baidu.cn</email>
</contact>
<contact id="2" vip="false">
<name>唐僧</name>
<gender>男</gender>
<email>tangtang@baidu.cn</email>
</contact>
<contact id="3" vip="false">
<name>猪八戒</name>
<gender>男</gender>
<email>zhuzhu@baidu.cn</email>
</contact>
<contact id="4" vip="false">
<name>白骨精</name>
<gender>女</gender>
<email>baibai@baidu.cn</email>
</contact>
<user>
<contact vip="true">
<name> 沙和尚 </name>
</contact>
</user>
</contactList>
四、设计模式
4.1 工厂模式
什么是工厂模式?
- 之前我们创建类对象时,都是使用new对象的形式创建,很多业务常见也提供了不直接new的方式
- 工厂模式(Factory Pattern)是Java常用的设计模式之一,这种类型的设计模式属于创建型模式,他提供了一种获取对象的方式。
作用:
- 工厂的方法可以封装对象的创建细节,比如:为该对象进行加工和数据注入。
- 可以实现类与类之间的解耦操作(核心思想)。
public class factoryPattern {
public static void main(String[] args) {
//此时我们创建了一个Computer接口,里面编写了电脑共同属性(name,price)和方法(start)
//以往创建电脑的写法如下:(需要我们一个个new对象,然后给相关属性)
// Mac mac = new Mac();
// mac.setName("苹果 puls");
// mac.setPrice(9999);
// mac.start();
//使用工厂设计模式,编写一个FactoryComputer类
Computer huawei = FactoryComputer.createComputer("huawei");
Computer mac = FactoryComputer.createComputer("mac");
huawei.start();
mac.start();
//华为16 pro电脑开机------
//苹果11 plus电脑启动了===
}
}
//工厂类
public class FactoryComputer {
//创建一个工厂方法,用来传入你选择的品牌,返回对应品牌电脑,且属性已经赋值,相当于封装了电脑的制作和属性
public static Computer createComputer(String brand){
switch (brand){
case "huawei":
HuaWei huaWei = new HuaWei();
huaWei.setName("华为16 pro");
huaWei.setPrice(8888);
return huaWei;
case "mac":
Mac mac = new Mac();
mac.setName("苹果11 plus");
mac.setPrice(19999);
return mac;
default:
return null;
}
}
}
//电脑抽象类
public abstract class Computer {
private String name;
private double price;
public void start() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
}
public class Mac extends Computer {
@Override
public void start() {
System.out.println(getName() + "电脑启动了===");
}
}
public class HuaWei extends Computer{
@Override
public void start() {
System.out.println(getName() + "电脑开机------");
}
}
4.2 装饰模式
什么是装饰设计模式?
- 创建一个新类,包装原始类,从而在新类中提升原来类的功能。
作用:
- 装饰模式指的是在不改变原类的基础上,动态扩展一个类的功能。
注意:装饰类和原始类必须继承同一个父类,然后装饰类是增强了原始类的功能
例如:前面我们所学的装饰类BufferedInputStream。我们可以模拟去创建装饰类。
public class DecorativePattern {
//装饰模式,我们模拟创建BufferedInputStream装饰类
//创建一个共同接口InputStream接口,定义公共方法read。
//实现接口,创建原始类FileInputStream,实现InputStream接口
//创建一个装饰类BufferedInputStream,也必须实现InputStream接口,加强FileInputStream的read功能
public static void main(String[] args) {
InputStream is = new BufferedInputStream(new FileInputStream());
System.out.println(is.read());
System.out.println(is.read(new byte[3]));
//开启8k缓冲区,开始一个个读字节
//低速度的读了一个字节a
//97
//开启8k的缓冲区,开始读一个字节数组
//低速度的读了一个字节数组
//3
}
}
//InputStream接口
public interface InputStream{
//模拟两个功能,读一个字节和一个字节数组
public int read();
public int read(byte[] buffer);
}
//原始类
public class FileInputStream implements InputStream{
@Override
public int read() {
System.out.println("低速度的读了一个字节a");
return 97;
}
@Override
public int read(byte[] buffer) {
buffer[0] = 'a';
buffer[1] = 'b';
buffer[2] = 'c';
System.out.println("低速度的读了一个字节数组");
//返回字节数组长度
return 3;
}
}
//装饰类
public class BufferedInputStream implements InputStream{
//创建一个InputStream来保存流
private InputStream is;
//传入输入流并保存这个输入流
public BufferedInputStream(InputStream is){
this.is = is ;
}
@Override
public int read() {
//模拟提前开启缓冲区,增加性能,然后再调用低速的输入流读
System.out.println("开启8k缓冲区,开始一个个读字节");
return is.read();
}
@Override
public int read(byte[] buffer) {
//模拟提前开启缓冲区,增加性能,然后再调用低速的输入流读
System.out.println("开启8k的缓冲区,开始读一个字节数组");
return is.read(buffer);
}
}
4.3 单例模式
设计模式中的一种,该模式可以保证系统中,应用该模式的这个类永远只有一个实例,即一个类永远只能创建一个对象。
- 例如:任务管理器对象我们只需要一个就可以解决问题了,这样可以节省内存空间,任务管理器只能打开一个,属于单例模式。
4.3.1 饿汉单例(eager)模式
在用类获取对象时,对象已经提前创建好了,提供一个公有方法返回该对象,且该类构造函数为私有,无法通过构造函数再次创建其他对象。
设计步骤:
- 定义一个类,把构造器私有;
- 定义一个静态变量存储一个对象;
- 提供公有静态方法,且方法返回该对象
//单例模式(饿汉模式)
public class EagerSingleton{
//因为static修饰的变量会跟着类一起加载,且只会加载一次,所以相当于只能new一次
private static EagerSingleton instance = new EagerSingleton();
//如果在new实例之前需要进行其他的初始化操作,可以将new对象放入静态代码块,因为静态代码块也只会随类加载一次
// static {
// instance = new EagerSingleton();
// }
//构造方法被私有化,私有化后就无法直接new对象,只能使用唯一的实例instance
private EagerSingleton(){
System.out.println("创建了有一个对象");
}
//提供获取实例的公有方法,返回值为类中提前创建的实例
public static EagerSingleton getInstance(){
return instance;
}
}
4.3.2 懒汉单例(lazy)模式(重点)
在使用该对象时,才去创建一个对象(延迟加载对象)。
**与饿汉单例模式区别为:**将new操作放入方法中,因为方法加载时不会创建对象,当调用方法后才会创建对象;
设计步骤 :
- 定义一个类,将构造方法私有;
- 定义一个静态变量存储一个对象;
- 提供一个静态方法,返回值为实例对象,且new操作会根据instance有无值来操作,当有值就不new直接返回;
//懒汉模式
class LazySingleton{
//创建一个静态变量,用于存放实例对象
private static LazySingleton instance ;
//构造方法私有化,防止new对象
private LazySingleton(){
System.out.println("创建了一个对象");
}
//提供公有方法,返回实例对象
public static LazySingleton getInstance(){
//判断是不是第一次调用方法,第一次调用就将new实例对象并存放到静态变量中
if (instance == null){
//第一次调用方法会进来,后面就不会再new对象,只会new一次
instance = new LazySingleton();
}
//返回实例对象
return instance;
}
}
注意:上面的创建懒汉单例模式可能会出现线程不安全,因为线程多了就可能会导致创建多个对象,解决线程不安全方法如下:
- 方法一:在new对象之前加锁,保证只有一个线程能够new对象;
- 方法二:可以将new对象放入内部类中(重点)
- 外部类加载时,内部类不会加载,当调用内部类的方法或者变量时才会加载
- 内部类中静态变量只会加载一次
方法一:
//懒汉模式(加锁)
class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {
System.out.println("创建了一个对象");
}
public static LazySingleton getInstance() {
//当第一次调用该方法就会为true,
//外层if判断可以提高性能,因为当不是第一次调用方法时,
//判断直接为true,就不会进入锁,以免每次都进入锁,其他线程等待,影响性能
if (instance == null) {
//加锁,解决线程不安全问题,锁为 类名.class
synchronized (LazySingleton.class) {
//内层if判断可以解决线程不安全问题,因为可能多个线程第一次都进入了if然后遇到锁就等待
//如果没有这个内层if,那么进入外层if的线程都会new对象
//所以内层嵌套if,可以防止多个线程进入if后,抢到锁还要判断实例是否被创建,如果被创建则后面线程都不会创建实例
if (instance == null)
instance = new LazySingleton();
}
}
return instance;
}
}
方法二: 相当于饿汉模式+内部类来实现线程安全的懒汉模式
//懒汉模式(加锁)
class LazySingleton {
private LazySingleton() {
System.out.println("创建了一个对象");
}
//提供公有获取实例方法
public static LazySingleton getInstance() {
//返回内部类中的变量(因为在内部类中new的对象)
return Inner.instance;
}
//创建内部类,将new对象封装到内部类中
//外部类加载,内部类不会加载,只有使用到了才会加载
private class Inner{
//new对象,用static修饰与类一起加载,且只加载一次,相当于只会new一次(所以不会出现线程不安全问题)
private static LazySingleton instance = new LazySingleton();
}
}