ORM概述

ORM(Object-Relational Mapping) 表示对象关系映射。在面向对象的软件开发中,通过ORM,就可以把对象映射到关系型数据库中。只要有一套程序能够做到建立对象与数据库的关联,操作对象就可以直接操作数据库数据,就可以说这套程序实现了ORM对象关系映射

简单的说:ORM就是建立实体类和数据库表之间的关系,从而达到操作实体类就相当于操作数据库表的目的。

为什么要使用ORM

当实现一个应用程序时(不使用O/R Mapping),我们可能会写特别多数据访问层的代码,从数据库保存数据、修改数据、删除数据,而这些代码都是重复的。而使用ORM则会大大减少重复性代码。对象关系映射(Object Relational Mapping,简称ORM),主要实现程序对象到关系数据库数据的映射。

常见的ORM框架

Mybatis(ibatis)、hibernate、Jpa

hibernate与JPA的概述

hibernate概述

Hibernate是一个开放源代码的对象关系映射框架,它对JDBC进行了非常轻量级的对象封装,它将POJO与数据库表建立映射关系,是一个全自动的orm框架,hibernate可以自动生成SQL语句,自动执行,使得Java程序员可以随心所欲的使用对象编程思维来操纵数据库。

JPA概述

JPA的全称是Java Persistence API, 即Java 持久化API,是SUN公司推出的一套基于ORM的规范,内部是由一系列的接口和抽象类构成。

JPA通过JDK 5.0注解描述对象-关系表的映射关系,并将运行期的实体对象持久化到数据库中。

JPA的优势

1. 标准化

JPA 是 JCP 组织发布的 Java EE 标准之一,因此任何声称符合 JPA 标准的框架都遵循同样的架构,提供相同的访问API,这保证了基于JPA开发的企业应用能够经过少量的修改就能够在不同的JPA框架下运行。

2. 容器级特性的支持

JPA框架中支持大数据集、事务、并发等容器级事务,这使得 JPA 超越了简单持久化框架的局限,在企业应用发挥更大的作用。

3. 简单方便

JPA的主要目标之一就是提供更加简单的编程模型:在JPA框架下创建实体和创建Java 类一样简单,没有任何的约束和限制,只需要使用 javax.persistence.Entity进行注释,JPA的框架和接口也都非常简单,没有太多特别的规则和设计模式的要求,开发者可以很容易的掌握。JPA基于非侵入式原则设计,因此可以很容易的和其它框架或者容器集成

4. 查询能力

JPA的查询语言是面向对象而非面向数据库的,它以面向对象的自然语法构造查询语句,可以看成是Hibernate HQL的等价物。JPA定义了独特的JPQL(Java Persistence Query Language),JPQL是EJB QL的一种扩展,它是针对实体的一种查询语言,操作对象是实体,而不是关系数据库的表,而且能够支持批量更新和修改、JOIN、GROUP BY、HAVING 等通常只有 SQL 才能够提供的高级查询特性,甚至还能够支持子查询。

5. 高级特性

JPA 中能够支持面向对象的高级特性,如类之间的继承、多态和类之间的复杂关系,这样的支持能够让开发者最大限度的使用面向对象的模型设计企业应用,而不需要自行处理这些特性在关系数据库的持久化。

JPA与hibernate的关系

JPA规范本质上就是一种ORM规范,注意不是ORM框架——因为JPA并未提供ORM实现,它只是制订了一些规范,提供了一些编程的API接口,但具体实现则由服务厂商来提供实现。

JPA和Hibernate的关系就像JDBC和JDBC驱动的关系,JPA是规范,Hibernate除了作为ORM框架之外,它也是一种JPA实现。JPA怎么取代Hibernate呢?JDBC规范可以驱动底层数据库吗?答案是否定的,也就是说,如果使用JPA规范进行数据库操作,底层需要hibernate作为其实现类完成数据持久化工作。

案例介绍

此处使用hibernate 5.4.10.Final   maven 3.6.3

导入依赖

 <dependencies>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>5.4.10.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-c3p0</artifactId>
<version>5.4.10.Final</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.30</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>

创建配置文件persistence.xml

注意:配置文件的目录

在java工程的src路径下创建一个名为META-INF的文件夹,在此文件夹下创建一个名为persistence.xml的配置文件

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="2.0"> <!--
需要配置persistence-unit 节点
持久化单元
name : 持久化单元名称
transaction-type : 事务管理方式
RESOURCE_LOCAL : 本地事务管理
JTA : 分布式事务管理
-->
<persistence-unit name="myJpa" transaction-type="RESOURCE_LOCAL">
<!-- jpa的实现方式 -->
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<properties>
<!--
配置数据库的信息
用户名 : javax.persistence.jdbc.user
密码 : javax.persistence.jdbc.password
连接地址 : javax.persistence.jdbc.url
驱动 : javax.persistence.jdbc.driver
-->
<property name="javax.persistence.jdbc.user" value="root"/>
<property name="javax.persistence.jdbc.password" value="root"/>
<property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/jpa"/>
<property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/> <!--配置jpa实现方(hibernate)的配置信息
显示sql : false|true
自动创建数据库表 : hibernate.hbm2ddl.auto
create : 程序运行时创建数据库表(如果有表,先删除表再创建)
update :程序运行时创建表(如果有表,不会创建表)
none :不会创建表 -->
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.hbm2ddl.auto" value="update"/>
</properties> </persistence-unit> </persistence>

创建实体类

package com.ytfs.entity;

import javax.persistence.*;
import java.io.Serializable; /**
* @Classname Customer
* @Description TODO(客户实体类)
* @Date 2020/4/28 22:34
* @Created by ytfs
* 客户的实体类
* 配置映射关系
* 1.实体类和表的映射关系
* @Entity:声明实体类
* @Table : 配置实体类和表的映射关系
* name : 配置数据库表的名称
* 2.实体类中属性和表中字段的映射关系
*
*/ @Entity
@Table(name = "cst_customer")
public class Customer implements Serializable {
/**
* @Id:声明主键的配置
* @GeneratedValue:配置主键的生成策略
* strategy
* GenerationType.IDENTITY :自增,mysql
* * 底层数据库必须支持自动增长(底层数据库支持的自动增长方式,对id自增)
* GenerationType.SEQUENCE : 序列,oracle
* * 底层数据库必须支持序列
* GenerationType.TABLE : jpa提供的一种机制,通过一张数据库表的形式帮助我们完成主键自增
* GenerationType.AUTO : 由程序自动的帮助我们选择主键生成策略
* @Column:配置属性和字段的映射关系
* name:数据库表中字段的名称
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "cust_id")
private Long custId; @Column(name = "cust_name")
private String custName; @Column(name = "cust_source")
private String custSource; @Column(name = "cust_industry")
private String custIndustry; @Column(name = "cust_level")
private String custLevel; @Column(name = "cust_address")
private String custAddress; @Column(name = "cust_phone")
private String custPhone; public Long getCustId() {
return custId;
} public void setCustId(Long custId) {
this.custId = custId;
} public String getCustName() {
return custName;
} public void setCustName(String custName) {
this.custName = custName;
} public String getCustSource() {
return custSource;
} public void setCustSource(String custSource) {
this.custSource = custSource;
} public String getCustIndustry() {
return custIndustry;
} public void setCustIndustry(String custIndustry) {
this.custIndustry = custIndustry;
} public String getCustLevel() {
return custLevel;
} public void setCustLevel(String custLevel) {
this.custLevel = custLevel;
} public String getCustAddress() {
return custAddress;
} public void setCustAddress(String custAddress) {
this.custAddress = custAddress;
} public String getCustPhone() {
return custPhone;
} public void setCustPhone(String custPhone) {
this.custPhone = custPhone;
} @Override
public String toString() {
return "Customer{" +
"custId=" + custId +
", custName='" + custName + '\'' +
", custSource='" + custSource + '\'' +
", custIndustry='" + custIndustry + '\'' +
", custLevel='" + custLevel + '\'' +
", custAddress='" + custAddress + '\'' +
", custPhone='" + custPhone + '\'' +
'}';
}
}

参数的解释

        @Entity
作用:指定当前类是实体类。
@Table
作用:指定实体类和表之间的对应关系。
属性:
name:指定数据库表的名称
@Id
作用:指定当前字段是主键。
@GeneratedValue
作用:指定主键的生成方式。。
属性:
strategy :指定主键生成策略。
@Column
作用:指定实体类属性和数据库表之间的对应关系
属性:
name:指定数据库表的列名称。
unique:是否唯一
nullable:是否可以为空
inserttable:是否可以插入
updateable:是否可以更新
columnDefinition: 定义建表时创建此列的DDL
secondaryTable: 从表名。如果此列不建在主表上(默认建在主表),该属性定义该列所在从表的名字搭建开发环境[重点]

创建JPA的连接工具类

package com.ytfs.utils;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence; /**
* @Classname Jpautil
* @Description TODO(Jpa实体类管理工厂)
* @Date 2020/4/28 23:38
* @Created by ytfs
*/
public class JpaUtil { //线程安全的
private static final EntityManagerFactory FACTORY; static { /*这里的实体类工厂的persistenceUnitName是配置文件中
<persistence-unit name="myJpa" transaction-type="RESOURCE_LOCAL">
*/
FACTORY = Persistence.createEntityManagerFactory("myJpa"); } /**
* 返回实体类管理对象
* @return
*/
public static EntityManager getEM(){
return FACTORY.createEntityManager();
}
}

创建测试类

package com.ytfs;

import com.ytfs.entity.Customer;
import com.ytfs.utils.JpaUtil;
import org.junit.Test; import javax.persistence.EntityManager;
import javax.persistence.EntityTransaction; /**
* @Classname test
* @Description TODO(JPA测试)
* @Date 2020/4/28 22:58
* @Created by ytfs
*/ public class test { /**
* 保存客户
*/
@Test
public void testSave() {
/* //创建实体类的工厂管理对象
EntityManagerFactory factory = Persistence.createEntityManagerFactory("myJpa"); //创建实体类管理对象
EntityManager entityManager = factory.createEntityManager();*/ EntityManager entityManager = JpaUtil.getEM();
//开启事务
entityManager.getTransaction().begin();
//创建一个对象
Customer customer = new Customer();
customer.setCustName("张三");
customer.setCustAddress("重庆市沙坪坝区");
//保存对象
entityManager.persist(customer); //提交事务
entityManager.getTransaction().commit(); //释放资源
entityManager.close();
//factory.close(); 利用工具类之后就不用关闭工厂对象,因为线程安全只需要一个,后面的还会一直用
} /**
* @throws
* @description find方式通过Id查询
* @author 雨听风说
* @updateTime 2020/4/29 13:03
* 使用find方法查询:
* 1.查询的对象就是当前客户对象本身
* 2.在调用find方法的时候,就会发送sql语句查询数据库
* <p>
* 立即加载
*/
@Test
public void testFind() { //通过工具类获取实体类管理对象
EntityManager em = JpaUtil.getEM(); //获取事务对象
EntityTransaction tx = em.getTransaction();
//开启事务
tx.begin(); //通过实体类管理对象查询
Customer customer = em.find(Customer.class, 1L); //customer.soutv
System.out.println("customer = " + customer); //提交事务
tx.commit(); //释放资源
em.close();
} /**
* @param
* @return
* @throws
* @description 通过事务管理对象的get方式通过Id查询
* @author 雨听风说
* @updateTime 2020/4/29 13:13
* getReference方法
* 1.获取的对象是一个动态代理对象
* 2.调用getReference方法不会立即发送sql语句查询数据库
* * 当调用查询结果对象的时候,才会发送查询的sql语句:什么时候用,什么时候发送sql语句查询数据库
* <p>
* 延迟加载(懒加载)
* * 得到的是一个动态代理对象
* * 什么时候用,什么使用才会查询
*/ @Test
public void testGetRefrence() { //通过工具类获取实体类管理对象
EntityManager em = JpaUtil.getEM(); //获取事务对象并开启事务
EntityTransaction tx = em.getTransaction();
tx.begin(); //通过实体类对象查询
Customer customer = em.getReference(Customer.class, 1L); System.out.println("customer = " + customer); //提交事务
tx.commit(); //释放资源
em.close();
} /**
* @param
* @return
* @throws
* @description 删除对象
* @author 雨听风说
* @updateTime 2020/4/29 13:17
*/
@Test
public void testRemove() { //通过工具类获取实体类管理对象
EntityManager em = JpaUtil.getEM(); //获取事务对象并开启事务
EntityTransaction tx = em.getTransaction();
tx.begin(); //通过实体类对象删除
//i 通过id查询需要删除的对象
Customer customer = em.find(Customer.class, 1L);
//ii 调用remove方法,传入需要删除的对象
em.remove(customer);
//提交事务
tx.commit(); //释放资源
em.close(); }
/**
* @param
* @return
* @throws
* @description 更新对象
* @author 雨听风说
* @updateTime 2020/4/29 13:17
*/
@Test
public void testUpdate() { //通过工具类获取实体类管理对象
EntityManager em = JpaUtil.getEM(); //获取事务对象并开启事务
EntityTransaction tx = em.getTransaction();
tx.begin(); //通过实体类对象更新
//i 通过id查询需要更新的对象
Customer customer = em.find(Customer.class, 2L);
customer.setCustName("电动小马达"); //ii 调用merge方法,传入需要更新的对象
em.merge(customer); //提交事务
tx.commit(); //释放资源
em.close();
}
}

JPQL查询(Java Persistence Query Language)

基于首次在EJB2.0中引入的EJB查询语言(EJB QL),Java持久化查询语言(JPQL)是一种可移植的查询语言,旨在以面向对象表达式语言的表达式,将SQL语法和简单查询语义绑定在一起·使用这种语言编写的查询是可移植的,可以被编译成所有主流数据库服务器上的SQL。

其特征与原生SQL语句类似,并且完全面向对象,通过类名和属性访问,而不是表名和表的属性。

测试

package com.ytfs;

import com.ytfs.utils.JpaUtil;
import org.junit.Test; import javax.persistence.EntityManager;
import javax.persistence.Query;
import java.util.List; /**
* @Classname JpqlTest
* @Description TODO(Jpql的测试案例)
* @Date 2020/4/29 13:22
* @Created by ytfs
*/ public class JpqlTest { /**
* jpql查询全部
* jqpl:from com.ytfs.entity.Customer
* sql:SELECT * FROM cst_customer
*/
@Test
public void testFindAll() { //通过工具类获取实体类管理对象
EntityManager em = JpaUtil.getEM(); //通过实体类管理对象获取事务对象并开启事务
em.getTransaction().begin(); //书写jpql语句
String jpql = "from Customer"; //通过实体类管理对象执行查询
Query query = em.createQuery(jpql); //获取查询结果
List list = query.getResultList(); //遍历输出
list.stream().forEach(System.out::println); //提交事务
em.getTransaction().commit(); //释放资源
em.close(); } /**
* 排序查询: 倒序查询全部客户(根据id倒序)
* sql:SELECT * FROM cst_customer ORDER BY cust_id DESC
* jpql:from Customer order by custId desc
*
* 进行jpql查询
* 1.创建query查询对象
* 2.对参数进行赋值
* 3.查询,并得到返回结果
*/
@Test
public void test() { //通过工具类获取实体类管理对象
EntityManager em = JpaUtil.getEM(); //通过实体类管理对象获取事务对象并开启事务
em.getTransaction().begin(); //书写jpql语句
String jpql = "from Customer order by custId desc"; //通过实体类管理对象执行查询
Query query = em.createQuery(jpql); //获取查询结果
List list = query.getResultList(); //遍历输出
list.stream().forEach(System.out::println); //提交事务
em.getTransaction().commit(); //释放资源
em.close(); } /**
* 使用jpql查询,统计客户的总数
* sql:SELECT COUNT(cust_id) FROM cst_customer
* jpql:select count(custId) from Customer
*/
@Test
public void testCount() { //通过工具类获取实体类管理对象
EntityManager em = JpaUtil.getEM(); //通过实体类管理对象获取事务对象并开启事务
em.getTransaction().begin(); //书写jpql语句
String jpql = "select count(custId) from Customer"; //通过实体类管理对象执行查询
Query query = em.createQuery(jpql); //ii.对参数赋值
//iii.发送查询,并封装结果 /**
* getResultList : 直接将查询结果封装为list集合
* getSingleResult : 得到唯一的结果集
*/
Object singleResult = query.getSingleResult(); //遍历输出
System.out.println("singleResult = " + singleResult); //提交事务
em.getTransaction().commit(); //释放资源
em.close(); } /**
* 分页查询
* sql:select * from cst_customer limit 0,2
* jqpl : from Customer
*/ @Test
public void testByPage() { //通过工具类获取实体类管理对象
EntityManager em = JpaUtil.getEM(); //通过实体类管理对象获取事务对象并开启事务
em.getTransaction().begin(); //书写jpql语句
String jpql = "from Customer"; //通过实体类管理对象执行查询
Query query = em.createQuery(jpql); //ii.对参数赋值
//开始的索引
query.setFirstResult(0);
//每页的条数
query.setMaxResults(2);
//iii.发送查询,并封装结果 /**
* getResultList : 直接将查询结果封装为list集合
* getSingleResult : 得到唯一的结果集
*/
//获取查询结果
List list = query.getResultList(); //遍历输出
list.stream().forEach(System.out::println); //提交事务
em.getTransaction().commit(); //释放资源
em.close(); } /**
* 条件查询
* 案例:查询客户名称包含‘小马’的客户
* sql:SELECT * FROM cst_customer WHERE cust_name LIKE ?1
* jpql : from Customer where custName like ?1
*/ @Test
public void testByCondition() { //通过工具类获取实体类管理对象
EntityManager em = JpaUtil.getEM(); //通过实体类管理对象获取事务对象并开启事务
em.getTransaction().begin(); //书写jpql语句
String jpql = "from Customer where custName like ?1 "; //通过实体类管理对象执行查询
Query query = em.createQuery(jpql); //ii.对参数赋值
//第一个参数:占位符的索引位置(jpql语句后的数字代表,直接问号出现异常),第二个参数:取值
query.setParameter(1, "%小马%");
//iii.发送查询,并封装结果 /**
* getResultList : 直接将查询结果封装为list集合
* getSingleResult : 得到唯一的结果集
*/
//获取查询结果
List list = query.getResultList(); //遍历输出
list.stream().forEach(System.out::println); //提交事务
em.getTransaction().commit(); //释放资源
em.close(); }
}

JPA与hibernate-------JPA01的更多相关文章

  1. JPA和hibernate的关系

    实际上,JPA的标准的定制是hibernate作者参与定制的,所以JPA是hibernate的一个总成,可以这么理解

  2. JPA与Hibernate的关系

    1.JPA JPA全称: Java Persistence API  JPA通过JDK 5.0注解或XML描述对象-关系表的映射关系,并将运行期的实体对象持久化到数据库中.  JPA的出现?  JPA ...

  3. JPA和Hibernate的区别

    JPA Java Persistence API,是Java EE 5的标准ORM接口,也是ejb3规范的一部分. Hibernate,当今很流行的ORM框架,是JPA的一个实现,但是其功能是JPA的 ...

  4. JPA入门例子(采用JPA的hibernate实现版本) 转

    JPA入门例子(采用JPA的hibernate实现版本) jpahibernate数据库jdbcjava框架(1).JPA介绍: JPA全称为Java Persistence API ,Java持久化 ...

  5. java(样品集成框架spring、spring mvc、spring data jpa、hibernate)

    这是你自己的参考springside集成框架的开源项目.主要的整合spring.spring mvc.spring data jpa.hibernate几个框架,对于这些框架中仍然感觉更舒适sprin ...

  6. 谈论Hibernate级联删除——JPA根据Hibernate实现许多级联删除CascadeType.DELETE_ORPHAN

    声明: 1.这篇文章是原创.非复制或转载过来. 2.在本文中,参数都亲自做过实验证明. 3.这篇文章谈到了Hibernate配置基于注释的方法.hbm语法不可用. 不清JPA.Hibernate.EJ ...

  7. 使用JPA和Hibernate进行批量处理的最佳方式

    Tips 原文作者:Vlad Mihalcea 原文地址:The best way to do batch processing with JPA and Hibernate 在本文中,你将了解什么是 ...

  8. SpringMVC+Apache Shiro+JPA(hibernate)整合配置

    序: 关于标题: 说是教学,实在愧不敢当,但苦与本人文笔有限,实在找不到更合理,谦逊的词语表达,只能先这样定义了. 其实最真实的想法,只是希望这个关键词能让更多的人浏览到这篇文章,也算是对于自己写文章 ...

  9. JPA(Hibernate)

    JPA 1,JPA:Java Persistence API.JPA通过JDK 5.0注解-关系表的映射关系,并将运行期的实体对象持久化到数据库中.JPA是JavaEE中的标准.JPA标准只提供了一套 ...

  10. spring data jpa、Hibernate开启全球唯一UUID设置

    spring data jpa.Hibernate开启全球唯一UUID设置 原文链接:https://www.cnblogs.com/blog5277/p/10662079.html 原文作者:博客园 ...

随机推荐

  1. java多线程7:ReentrantReadWriteLock

    真实的多线程业务开发中,最常用到的逻辑就是数据的读写,ReentrantLock虽然具有完全互斥排他的效果(即同一时间只有一个线程正在执行lock后面的任务), 这样做虽然保证了实例变量的线程安全性, ...

  2. 攻防世界 pwn welpwn

    感觉好久没有水博客了,今天借助这道题来告诉自己做pwn题要多调试!!! 先检查了保护只开启了堆栈不可执行,接下来ida看一下伪代码: 这里可以往buf进行写入,接下来看一下echo函数: 大概意思就是 ...

  3. CF125A Measuring Lengths in Baden 题解

    Content 在 Baden,一英寸等于 \(3\) 厘米,一英尺等于 \(12\) 英寸. 现在有一个 \(n\) 厘米的物体,求在 Baden,它是几英尺又几英寸. 数据范围:\(1\leqsl ...

  4. UVA12412 师兄帮帮忙 A Typical Homework (a.k.a Shi Xiong Bang Bang Mang) 题解

    Content 自己去看题面去. Solution 算不上很繁琐的一道大模拟. 首先,既然是输出 \(0\) 才退出,那么在此之前程序应当会执行菜单 \(\Rightarrow\) 子操作 \(\Ri ...

  5. CF1438A Specific Tastes of Andre 题解

    Content 如果一个序列的和能够被它的长度整除,我们称这个序列是不错的.如果一个序列的所有的非空子序列都是不错的,我们就称这个序列是完美的.现在有 \(t\) 组询问,每组询问给定一个整数 \(n ...

  6. BERT生成能力改进:分离对话生成和对话理解

    NLP论文解读 原创•作者 | 吴雪梦Shinemon 研究方向 | 计算机视觉 导读说明: NLP任务大致可以分为NLU(自然语言理解)和NLG(自然语言生成)两种,NLU负责根据上下文去理解当前用 ...

  7. 反解ios静态库

    p.p1 { margin: 0; font: 12px "Helvetica Neue" } p.p2 { margin: 0; font: 12px "Helveti ...

  8. 【LeetCode】731. My Calendar II 解题报告(Python)

    [LeetCode]731. My Calendar II 解题报告(Python) 作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 题 ...

  9. 【LeetCode】729. My Calendar I 解题报告

    [LeetCode]729. My Calendar I 解题报告 标签(空格分隔): LeetCode 题目地址:https://leetcode.com/problems/my-calendar- ...

  10. 【剑指Offer】序列化二叉树 解题报告(Python)

    [剑指Offer]序列化二叉树 解题报告(Python) 标签(空格分隔): 剑指Offer 题目地址:https://www.nowcoder.com/ta/coding-interviews 题目 ...