ORM即Object-Relationl Mapping,它的作用是在关系型数据库和对象之间作一个映射,这样,我们在具体的操作数据库的时候,就不需要再去和复杂的SQL语句打交道,只要像平时操作对象一样操作它就可以了 。
在ORM出现之前,我们是用jdbc来操作数据库,但jdbc没有封装,对于大项目来说,使用复杂,也难以实现MVC的概念,所以人们就开发了ORM框架来解决这些问题。比如Hibernate,Mybatis,不过Hibernate是完全的ORM框架,mybatis是半ORM框架,因为它需要手动建表和自己写sql。
ORM的优点: 提高了开发效率。由于ORM可以自动对Entity对象与数据库中的Table进行字段与属性的映射,能够像操作对象一样从数据库获取数据。
ORM的缺点 :ORM的缺点是会牺牲程序的执行效率,因为是自动生成sql,所以实现复杂查询比较麻烦。
ORM 框架很多,大家各自搞自己的,为了统一下规范,就出现了 JPA。
JPA全称Java Persistence API,可以通过注解或者XML描述【对象-关系表】之间的映射关系,并将实体对象持
久化到数据库中。
JPA为我们提供了:
1)ORM映射元数据:JPA支持XML和注解两种元数据的形式,元数据描述对象和表之间的映射关系,框架据此将实体对象持久化到数据库表中;
如:@Entity、@Table、@Column、@Transient等注解。
2)API:用来操作实体对象,执行CRUD操作,框架在后台替我们完成所有的事情,开发者从繁琐的JDBC和SQL代码中解脱出来。
如:entityManager.merge(T t);
3)JPQL查询语言:通过面向对象而非面向数据库的查询语言查询数据,避免程序的SQL语句紧密耦合。
如:from Student s where s.name=?
但是:
JPA仅仅是一种规范,也就是说JPA仅仅定义了一些接口,而接口是需要实现才能工作的。所以底层需要某种实现,而Hibernate就是实现了JPA接口的ORM框架。
实现Jpa中的接口需要写大量的代码,包括简单的增删改查,那可不可以由框架将这些写好呢,于是spring data jpa出现了。
spirng data jpa是spring提供的一套简化JPA开发的框架,不仅有接口,也有实现类,只要按照约定好的【方法命名规则】写dao层接口,就可以在不写接口实现的情况下,实现对数据库的访问和操作。同时提供了很多除了CRUD之外的功能,如分页、排序、复杂查询等等。
Spring Data JPA 可以理解为 JPA 规范的再次封装抽象,底层还是使用了 Hibernate 的 JPA 技术实现。
关于在开发中到底应该使用JPA还是Mybatis争论不休,总体来说,国外用JPA的多,国内用Mybatis的多。
Spring Data JPA是面向对象的思想,一个对象就是一个表,强化的是你对这个表的控制。spring data jpa实现了jpa(java persistence api)功能,即可以实现pojo转换为关系型数据库记录的功能,通俗来讲就是可以不写任何的建表sql语句了。jpa是spring data jpa功能的一个子集。
Mybatis则是面向sql,你的结果完全来源于sql,而对象这个东西只是用来接收sql带来的结果集。你的一切操作都是围绕sql,包括动态根据条件决定sql语句等。mybatis并不那么注重对象的概念。只要能接收到数据就好。
各自优缺点:
面向sql就更利于优化,因为sql可以优化的点太多了。对于并发用户多,追求性能的,Mybatis更有优势。
面向对象就更利于移植,可维护性,因为数据对象不依赖于数据源。比如从mysql换成oracle,JPA更方便。
最终用哪个,看你们老板的要求。
Spring Boot 中使用的 Jpa 实际上是 Spring Data Jpa,在 Spring Data 中,只要你的方法名称符合规范,它就知道你想干嘛,不需要自己再去写 SQL。本章带领大家学习如何在Springboot中如何集成spring data jpa
创建工程,添加 Web、Spring Data Jpa 、 MySQL 驱动依赖、lombok依赖。
? 默认Mysql驱动时8.X,如果本机安装的Mysql是5.X,需要手动修改版本
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.45</version> <!-- 手动添加版本号-->
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
springboot默认的连接池是HiKari,但国内用druid的也很多,所以本章中我们也用一下druid。
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
常见的数据库连接池主要有c3p0,dbcp,tomcat-jdbc-pool,druid,HiKariCP。
c3p0:不提供对数据库的监控。使用时是单线程的。
dbcp:不提供数据库的监控。使用时是单线程的。
tomcat jdbc pool:它兼容dbcp。但是比dbcp性能更高。
druid: 是阿里巴巴开源的数据库连接池,提供对数据库的监控,就是为监控而生。它的功能最为全面,可扩展性好,具有sql拦截的功能。
HiKariCP: 是数据库连接池里面的后起之秀,出来的比较晚,但是性能很好。
总的来说:性能方面HiKariCP>druid>tomcat jdbc pool>dbcp>c3p0。
# 数据库的基本配置
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.url=jdbc:mysql://localhost:3306/boot?characterEncoding=utf8&serverTimezone=GMT%2B8
#配置连接池
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
# JPA配置
spring.jpa.database=mysql
# 是否在控制台打印SQL
spring.jpa.show-sql=true
# 每次启动项目时,数据库初始化策略
#ddl-auto:create----每次运行该程序,没有表格会新建表格,表内有数据会清空
#ddl-auto:create-drop----每次程序结束的时候会清空表
#ddl-auto:update----每次运行程序,没有表格会新建表格,表内有数据不会清空,只会更新
#ddl-auto:validate----运行程序会校验数据与数据库的字段类型是否相同,不同会报错
spring.jpa.hibernate.ddl-auto=update
# 指定默认的存储引擎为InnoDB,默认情况下,自动创建表的时候会使用 MyISAM 做表的引擎,
# 如果配置了数据库方言为 MySQL57Dialect,则使用 InnoDB 做表的引擎。
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL57Dialect
?
先准备数据库表:
创建对应的实体类,添加相应注解:
package com.test.jpa.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import javax.persistence.*;
@Entity(name="users")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Users {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private int id;
@Column(name="user_name")
private String userName;
private int age;
}
?
编写Dao接口:
package com.test.jpa.dao;
import com.test.jpa.pojo.Users;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UsersRepository extends JpaRepository<Users,Integer> {
}
测试:
编写web层测试:
总结:
通过入门案例我们可以发现,dao层只需要继承JpaRepository接口,我们不需要写sql语句就可以查询出数据。这就是Jpa的方便之处。
提供了方法名称命名查询方式
?提供了基于@Query注解查询与更新
? 需要自定义方法名,如下所示:
import java.util.List;
public interface UsersRepository extends Repository<Users,Integer> {
List<Users> findAll();
}
测试:
这里省掉了service层,直接在控制层测试
CrudRepository接口继承了Repository接口
CrudRepository提供了基本的增删改查,不再需要我们自定义。
import com.bjsxt.pojo.Users;
import org.springframework.data.repository.CrudRepository;
public interface UsersRepositoryCrudRepository extends CrudRepository<Users,Integer> {
}
该接口继承了CrudRepository接口
该接口提供了分页与排序的操作, 也就是该接口不用自己定义增删改查方法和分页排序方法
import com.bjsxt.pojo.Users;
import org.springframework.data.repository.PagingAndSortingRepository;
public interface UsersRepositoryPagingAndSorting extends PagingAndSortingRepository<Users,Integer> {
}
? 该接口继承了PagingAndSortingRepository
? 对继承的父接口中方法的返回值进行适配,也就是该接口不用自己定义增删改查方法和分页排序方法,并且让分页查询更简单。
该接口主要是提供了多条件查询的支持,并且可以在查询中添加排序与分页。注意JPASpecificationExecutor是单独存在的。不继承上述接口。
可以看出来,因为JpaRepository继承了前面几个接口,所以我们重点来研究JpaRepository和JPASpecificationExecutor就可以了。
? dao层:
package com.test.jpa.dao;
import com.test.jpa.pojo.Users;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UsersJpaRepository extends JpaRepository<Users,Integer> {
}
web层测试:
package com.test.jpa.controller;
import com.test.jpa.dao.UsersJpaRepository;
import com.test.jpa.pojo.Users;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/users")
public class UsersController {
@Autowired
private UsersJpaRepository usersJpaRepository;
@RequestMapping("/select")
public List<Users> select()
{
return usersJpaRepository.findAll();
}
@RequestMapping("/add")
public void addUser()
{
Users user=new Users();
user.setAge(19);
user.setUserName("cc");
usersJpaRepository.save(user);
}
@RequestMapping("/update")
public void updateUser()
{
Users user=usersJpaRepository.getOne(3);
user.setAge(19);
user.setUserName("ddd");
usersJpaRepository.save(user);
}
@RequestMapping("/delete")
public void deleteUserById()
{
int id=1;
usersJpaRepository.deleteById(id);
}
@RequestMapping("/selectById")
public Users selectById()
{
return usersJpaRepository.findById(3).get();//
// 2.x版本后需要.get()才能得到实体对象, id未查询到对应实体时会报错
}
}
//分页查询
@RequestMapping("/selectByPage")
public List<Users> selectByPage() {
PageRequest pageable=PageRequest.of(0, 2);
Page<Users> page=usersJpaRepository.findAll(pageable);
List<Users> usersList=page.getContent();
//数据的总条数:page.getTotalElements();
//总页数:page.getTotalPages();
return usersList;
}
其中:
PageRequest:封装了分页的参数,当前页,每页显示的条数。注意:它的当前页是从0开始
Page :是spring data jpa 的分页模型。包含的属性信息如下:
{
"content": [{}], // 数据列表
"last": true, // 是否最后一页
"totalPages": 1, // 总页数
"totalElements": 1, // 数据总数
"sort": null, // 排序
"first": true, // 是否首页
"numberOfElements": 1, // 本页数据条数
"size": 10, // 每页长度
"number": 0 // 当前页序号
}
分页并排序:
//分页查询并排序
@RequestMapping("/selectByPageByOrder")
public List<Users> selectByPageByOrder() {
//按照id降序排
PageRequest pageable=PageRequest.of(0, 2,Sort.Direction.DESC, "id");
Page<Users> page=usersJpaRepository.findAll(pageable);
List<Users> usersList=page.getContent();
//数据的总条数:page.getTotalElements();
//总页数:page.getTotalPages();
return usersList;
}
JpaRepository 提供了一些基本的数据操作方法,例如保存,更新,删除,分页查询等,开发者也可以在接口中自己声明相关的方法,只需要方法名称符合规范即可,在 Spring Data 中,只要按照既定的规范命名方法,Spring Data Jpa 就知道你想干嘛,这样就不用写 SQL 了,那么规范是什么呢?参考下图:
在UsersJpaRepository接口中添加自定义方法:
List<Users> findByUserName(String userName);
List<Users> findByUserNameLike(String userName);
测试:
有的时候,Spring Data规范里提供的查询关键字并不能满足我们的查询需求,这个时候就可以使用 @Query 关键字,来自定义查询 SQL。
1.例如查询 id 最大的 User:
@Query(value="select * from users where id=(select max(id) from users)",nativeQuery=true)
Users getMaxIdUser();
nativeQuery:代表本地查询,就是使用原生的sql语句。
用来注入参数
@Query(value="select * from users where user_name like %:userName%",nativeQuery=true)
List<Users> findByNameMatch(@Param("userName") String userName);
?
Spring Data JPA中也有一对一、一对多、多对多映射。这些映射还分单向关联和双向关联,在双向关联时还需要考虑对象序列化为JSON字符串时的死循环问题。
单向关联和双向关联
一对一映射需要@OneToOne注解和@JoinColumn注解配合使用
准备两张表users和cards:
创建实体类:
Cards:
package com.test.jpa.pojo;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@Entity(name="cards")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Cards {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY )
private int id;
@Column(name="card_type")
private String cardType;
}
Users:
每个用户对应一张会员卡,所以每个users对象中包含一个card对象。
package com.test.jpa.pojo;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import javax.persistence.*;
import java.io.Serializable;
@Entity(name="users")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Users {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private int id;
@Column(name="user_name")
private String userName;
private int age;
//@JoinColumn注解中的name元素为当前实体类中对应的属性id,即users表中的card_id
// 而referencedColumnName则为关联对象的id,即cards表中的主键id
@JoinColumn(name="card_id",referencedColumnName="id")
@OneToOne(cascade={CascadeType.ALL})
private Cards card;
}
注意:
@JoinColumn 是指表与表之间关系的字段
@OneToOne是一对一关系映射。其中CascadeType是级联类型。
CascadeType.PERSIST
级联新增,保存父对象时会新建其中包含的子对象
CascadeType.MERGE
级联修改,保存父对象时会更新其中所包含的子对象数据
CascadeType.REMOVE
级联删除,当删除关联关系时会将子对象的数据删除
CascadeType.REFRESH
级联刷新,保存关联关系时会更新子对象和数据库中一致(意思是你在父对象中添加一个只包含ID的子对象,也可以保存进去)
CascadeType.ALL
包含上述所有操作
新增address表
这里一个用户对应多个地址,是一对多的关系。
描述一对多关系中需要用到@OneToMany和@ManyToOne
首先创建Address实体类:
package com.test.jpa.pojo;
import com.fasterxml.jackson.annotation.JsonBackReference;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import javax.xml.soap.Detail;
@Data
//jpa中使用lombok时,需排除关联表属性,否则会报错
@EqualsAndHashCode(exclude="user")
@NoArgsConstructor
@AllArgsConstructor
@Entity(name="address")
public class Address {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private int id;
private String detail;
@ManyToOne(cascade=CascadeType.ALL,optional=false)////可选属性optional=false,表示user不能为空。删除地址,不影响用户
@JoinColumn(name="user_id")
@JsonBackReference //防止json序列化出现死循环
private Users user;
}
注意:此实体类中添加了三个新注解
@EqualsAndHashCode(exclude="user") 是指 jpa中使用lombok时,需排除关联表属性,否则会报错。
@ManyToOne()代表多对一的关系
@JsonBackReference 防止json序列化出现死循环
在Users表中添加address集合
package com.test.jpa.pojo;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import javax.persistence.*;
import java.io.Serializable;
import java.util.List;
@Entity(name="users")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Users {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private int id;
@Column(name="user_name")
private String userName;
private int age;
//@JoinColumn注解中的name元素为当前实体类中对应的属性id,即users表中的card_id
// 而referencedColumnName则为关联对象的id,即cards表中的主键id
@JoinColumn(name="card_id",referencedColumnName="id")
@OneToOne(cascade={CascadeType.ALL})
private Cards card;
@OneToMany(mappedBy="user")
private List<Address> addressList;
}
注意:这里新增了一个注解
@OneToMany(mappedBy="user") 代表一对多的关系
其中属性mappedBy的意思是:
1.只有OneToOne,OneToMany,ManyToMany上才有mappedBy属性,ManyToOne不存在该属性,在@OneToMany里加入mappedBy属性可以避免生成一张中间表。
2.mappedBy标签一定是定义在被拥有方的,他指向拥有方; 表示声明自己不是一对多的关系维护端,由对方来维护,是在一的一方进行声明的。mappedBy的值应该为一的一方的表名
3.mappedBy的含义,应该理解为,拥有方能够自动维护跟被拥有方的关系,当然,如果从被拥有方,通过手工强行来维护拥有方的关系也是可以做到的;
4.mappedBy跟joinColumn/JoinTable总是处于互斥的一方,可以理解为正是由于拥有方的关联被拥有方的字段存在,拥有方才拥有了被拥有方。mappedBy这方定义JoinColumn/JoinTable总是失效的,不会建立对应的字段或者表。
测试:
准备任务表和用户和任务的关系表,一个用户对应多个任务,一个任务对应多个用户,形成多对多的关系.
创建实体类:Tasks
package com.test.jpa.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity(name="tasks")
public class Tasks {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private int id;
private String content;
}
修改Users实体类,添加多对多关系
package com.test.jpa.pojo;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import javax.persistence.*;
import java.io.Serializable;
import java.util.List;
import java.util.Set;
@Entity(name="users")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Users {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private int id;
@Column(name="user_name")
private String userName;
private int age;
//@JoinColumn注解中的name元素为当前实体类中对应的属性id,即users表中的card_id
// 而referencedColumnName则为关联对象的id,即cards表中的主键id
@JoinColumn(name="card_id",referencedColumnName="id")
@OneToOne(cascade={CascadeType.ALL})
private Cards card;
@OneToMany(mappedBy="user")
private List<Address> addressList;
@ManyToMany
@JoinTable(name="user_task",joinColumns=@JoinColumn(name="user_id"),
inverseJoinColumns=@JoinColumn(name="task_id"))
private List<Tasks> tasksList;
}
注意:这里新增了两个注解
@ManyToMany 代表多对多的关系
@JoinTable 存放的是两个实体间的多对多关系表,name中存放的是关系表表名, joinColumns存放的当前实体类在关心表中的id名,inverseJoinColumns存放的是关联的实体表在关系表中的id名.
测试:
? 该接口主要是提供了多条件查询的支持,并且可以在查询中添加排序与分页。它是独立存在的。
? 多条件查询示例:
添加dao层
package com.test.jpa.dao;
import com.test.jpa.pojo.Users;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface UsersSpecificationExecutor extends JpaRepository<Users,Integer>,JpaSpecificationExecutor<Users> {
}
控制层测试:
package com.test.jpa.controller;
import com.test.jpa.dao.UsersSpecificationExecutor;
import com.test.jpa.pojo.Users;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.swing.text.html.HTMLDocument;
import java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("/specification")
public class TestSpecificationExecutor {
@Autowired
private UsersSpecificationExecutor usersSpecificationExecutor;
@RequestMapping("/select")
public List<Users> select()
{
Specification<Users> usersSpecification=new Specification<Users>(){
//CriteriaBuilder是一个工厂类,用来创建安全查询的criteriaQuery对象和拼接的查询条件
//Root根对象对应于from后面的表
//Predicate用于查询条件的拼接,对应于where后面的表达式。
@Override
public Predicate toPredicate(Root<Users> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
List<Predicate> list=new ArrayList<>();
list.add(criteriaBuilder.equal(root.get("userName"),"aa"));
list.add(criteriaBuilder.equal(root.get("age"),11));
Predicate[] arr=new Predicate[list.size()];
return criteriaBuilder.and(list.toArray(arr));
}
};
List<Users> list=this.usersSpecificationExecutor.findAll(usersSpecification);
return list;
}
}
? JPQL全称Java Persistence Query Language。中文意思是Java持久化查询语言。
? 是一种可移植的查询语言,旨在以面向对象表达式语言的表达式,将SQL语法和简单查询语义绑定在一起·使用 这种语言编写的查询是可移植的,可以被编译成所有主流数据库服务器上的SQL。
其特征与原生SQL语句类似,并且完全面向对象,通过类名和属性访问,而不是表名和表的属性。
查询用的 SELECT 语法
比如:SELECT u FROM Users u WHERE u.userName=:userName
? 对应sql: select * from users where user_name=参数
解析: 这里的Users是指实体 u是别名。u.userName是实体对象中的userName属性。:userName是传递的参数。
更新用的 UPDATE 语法
比如:UPDATE Users u SET u.userName=:userName where u.id=:id
删除用的 DELETE 语法
比如: delete from Users u where u.id=:id
注意:JPQL中没有insert添加语句
创建接口UsersDao3,添加查询方法.
package com.test.springdatajpa.dao;
import com.test.springdatajpa.pojo.Users;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
public interface UsersDao3 extends JpaRepository<Users,Integer> {
//查询
@Query("SELECT u FROM users u WHERE u.userName=:userName")
//简化写法
//@Query(" FROM users u WHERE u.userName=:userName")
public List<Users> getUsers(@Param("userName") String userName);
}
注意:
@Query(“SELECT u FROM users u WHERE u.userName=:userName”) 中的users是实体名,而实体名用哪个有两种情况:
情况1:在实体类中直接用@Entity(name=“users”) 不用@Table ,则表示此实体对应的表名是users,同时实体名也设置为users。如下:
情况2:在实体类中用
@Entity
@Table(name=“users”) 两个注解,则表示此实体对应的表名是users,实体名为类名。如下:
调用dao层方法测试:
在UsersDao3中添加修改和删除:
package com.test.springdatajpa.dao;
import com.test.springdatajpa.pojo.Users;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
public interface UsersDao3 extends JpaRepository<Users,Integer> {
//查询
//@Query("SELECT u FROM users u WHERE u.userName=:userName")
@Query(" FROM users u WHERE u.userName=:userName")
public List<Users> getUsers(@Param("userName") String userName);
//修改
//SpringData 的每个方法上有事务, 但都是一个只读事务。 他们不能完成修改操作,所以需要手动添加@Transactional
@Transactional
@Query( "UPDATE users u SET u.userName=:userName where u.id=:id")
@Modifying
public void updateUsers(@Param("id") int id, @Param("userName") String userName);
//删除
@Transactional
@Query( "delete from users u where u.id=:id")
@Modifying
public void deleteUsers(@Param("id") int id);
}
注意:
在使用修改和删除语句的时候。需要加@Modifying和@Transactional注解
@Transactional:是因为SpringData 的每个方法上有事务, 但都是一个只读事务。 他们不能完成修改操作,所以需要手动添加
@Modifying:JPQL实现DELETE和UPDATE操作的时候必须加上@modifying注解,以通知Spring Data 这是一个DELETE或UPDATE操作。
呼~,历过好几天的奋战终于把集合框架肝完了,b站某马老师讲的是真的非常详细而且动听,原理给你分析得明明白白的,此前也找了许多关于集合这一大章节的视频,发现更多的是针对于使用,原理讲的并不是很多,这就导致我在练习或者回顾时还是一知半解。以下是我结合视频以及个人的一些理解和体会做的笔记总结。路漫漫其修远兮,吾将上下而求索,希望这篇总结对你有些许帮助,嘻嘻!
集合:Java中提供的一种容器,可以用来存储多个数据。java集合大致可以分为Set,List,Queue和Map四种体系。
数组和集合的区别:
单列集合体系结构:
Collection接口是所有单列集合的父接口,因此在单列集合中定义的List和set通用的一些方法,这些方法可以操作所有的单列集合。方法如下:
【参考代码】
package Collection; import java.util.ArrayList; import java.util.Collection; /* Collection集合常用方法 boolean add(E e); 向集合中添加元素 boolean remove(E e); 删除集合中的某个元素 void clear(); 清空集合中所有的元素 boolean contains(); 判断集合中是否含有xxx元素 boolean isEmpty(); 判断集合是否为空 int size(); 计算集合的长度 Object[] toArray(); 将集合转成一个数组 */ public class Test { public static void main(String[] args) { //创建集合对象 , 可以多态使用 Collection<String>col=new ArrayList<>(); // Collection<String>col=new HashSet<>(); 下面的功能照样能实现:共性方法 col.add("小明"); // 添加元素 col.add("小红"); col.add("小蓝"); col.add("小绿"); System.out.println(col); //[小明, 小红, 小蓝, 小绿] //boolean remove(E e); 删除集合中的某个元素 // boolean ans=col.remove("小明"); // System.out.println(ans);//true // System.out.println(col);//[小红, 小蓝, 小绿] //void clear(); 清空集合中所有的元素 // col.clear(); // System.out.println(col);//[] //boolean contains(); 判断集合中是否含有xxx元素 // boolean result=col.contains("小明"); // System.out.println(result);//true //boolean isEmpty(); 判断集合是否为空 // boolean result=col.isEmpty(); // System.out.println(result);// 不为空false //int size(); 计算集合的长度 // int len=col.size(); // System.out.println(len);// 4 //Object[] toArray(); 将集合转成一个数组 Object[] arr=col.toArray(); // 遍历数组 // for (int i=0; i < arr.length; i++) { // System.out.println(arr[i]); // } } }
引入:由于集合有多种,每种集合存储跟读取的方式都不一样,好比衣柜、水瓶、药瓶,你存和取的方式肯定不一样。如果每种集合都定义一种遍历方式那将十分的繁琐。
迭代器(Iterator):它不是一个容器而是接口,它是一种用于访问容器的方法,可用于迭代 List、Set和Map等容器。
迭代:即Collection集合的通用获取方式。再获取元素之前先要判断集合中是否有元素,如果有就将这个元素去取出来,继续再判断,直到集合所有元素被取出来为止。即:一个一个的往外拿。
作用:帮我们遍历或者拿到容器里边的数据。
迭代器常用操作:
迭代器的使用步骤:
【参考代码】
package Iterator; import javax.swing.text.html.parser.Entity; import java.util.*; public class Test { public static void main(String[] args) { //创建一个集合对象 Collection<String>col=new ArrayList(); //添加元素 col.add("小明"); col.add("小红"); col.add("小蓝"); col.add("小绿"); /* 1.使用集合的方法iterator()获取迭代器的实现类对象,使用Iterator接口接收(多态) 注意: Iterator接口也是有泛型的,迭代器的泛型跟集合走,集合是什么泛型,迭代器就是什么泛型 */ // 多态 接口 实现类对象 Iterator<String>it=col.iterator(); // 2.使用 Iterator接口中的hashNext方法判断是否还有下一个元素 while(it.hasNext());{ // 3.使用 Iterator接口中的next方法取出集合的下一个元素 String str=it.next(); System.out.println(str); } } }
增强for循环(for each循环)是JDk1.5之后的一个高循环,专门用来遍历数组和集合的,所有的数组跟单列集合都可以使用。它的内部原理就是一个迭代器Iterator,所以在遍历过程中,不能对集合元素进行增删操作。
语法:
for(类型 变量 : 数组/集合){// 数组或者集合里的每一项赋值给这个变量
// 循环体
}
【参考代码】
String[] student={"小明","小红","小蓝"}; // // 传统遍历方式 // for (int i=0; i < student.length; i++) { // System.out.println(student[i]); // } // 增强for for(String c : student){ System.out.println(c); } -------------------------------- List<Integer>list=new ArrayList<Integer>(); list.add(123); list.add(234); list.add(456); for(Integer n : list){ System.out.println(n); }
注:增强for必须有被遍历的目标。目标只能是数组或者Collection,而它仅仅作为遍历操作实现
在前面学习集合时,我们知道集合时可以存放任意对象的,只要把对象存储集合后,它们都会被向上转型提升为Object类型。当我们要取出这些对象时必须进行类型强制转换,由Object类型变为原来的类型。
不使用泛型:
- 好处:集合默认类型是Object类,可以存储任意类型的数据
- 弊端:不安全,会引发异常,需要强转。
public static void main(String[] args) { List list=new ArrayList(); list.add("小明"); list.add("小红"); for (int i=0; i < list.size(); i++) { String s=(String)list.get(i) // 强转 System.out.println(s); } }
使用泛型:
public static void main(String[] args) { List<String> list=new ArrayList();// 规范了数据类型,只能放字符串! list.add("小明"); list.add("小红"); //stringList.add(123);// 除了字符串以外的类型不能加,报错! for (int i=0; i < list.size(); i++) { String s=list.get(i); // 不用再强转了 System.out.println(s); } }
在上述的实例中,我们只能添加String类型的数据,否则编译器会报错。
定义格式:
修饰符 class 类名<泛型变量>{ } // 注:泛型变量建议使用E、T、K、V
例如:
public class Box<T> {
private T t;
public void add(T t) {
this.t=t;
}
public T get() {
return t;
}
参考示例:
注:在创建对象时确定泛型的类型
定义格式:
修饰符 <泛型变量> 返回值的类型 方法名称(形参列表){
//方法体
}
注:含有泛型的方法,在调用的时候确定泛型的数据类型
传递什么类型的参数,泛型就是什么类型
参考示例:
定义格式:
public interface 接口名<泛型类型> {
}
使用方式1:定义接口的实现类,实现接口,并且指定接口的泛型
使用方式2:接口使用什么泛型,实现类就使用什么泛型,类跟着接口走。
就相当于定义了一个含有泛型的类,创建对象的时候确定泛型的类型。
下图接口同上图接口
当使用泛型类或接口时,传递数据中,泛型类型不确定,可以通过通配符表示<?>表示。但一旦使用泛型的通配符后,只能使用Object类中的共性方法,集合中元素自身方法无法使用。
泛型的通配符:不知道使用什么类型来接收的时候,此时可以使用 ? ,?表示未知通配符
此时只能接收数据,不能往集合中存储数据。
【参考代码】
package FanXing; import javax.swing.text.html.HTMLDocument; import java.util.ArrayList; import java.util.Iterator; /* 泛型的通配符: ?:代表数据类型 使用方式: 不能在创建对象时使用 只能作为方法的传递参数使用 */ public class Generic { public static void main(String[] args) { ArrayList<Integer>list01=new ArrayList<>(); list01.add(123); list01.add(456); ArrayList<String>list02=new ArrayList<>(); list02.add("小明"); list02.add("小红"); // ......还有很多其它类型 printArray(list01); printArray(list02); /* 定义一个方法,能遍历所有类型的ArrayList集合 这时候我们不知道ArrayList集合使用的是什么类型,可以使用泛型的通配符:?来代表数据类型 注意:泛型没有继承的概念 */ } public static void printArray(ArrayList<?>list){ // 使用迭代器遍历集合 Iterator<?> it=list.iterator(); while(it.hasNext()){ Object obj=it.next();//it.next()取出的元素是Object类。Object类 可以接收任意的数据类型 System.out.println(obj); } } }
之前设置泛型的时候,实际上是可以可以任意设置的,只要是类就可以设置。但在Java的泛型中可以指定一个泛型的上限和下限。
泛型的上限:
泛型的下限:
比如:Object类、String类、Number类、Integer类,其中Number类是Integer的父类。
集合是基于数据结构做出来的,不同的集合底层采用不同的数据结构。不同的数据结构,功能和作用是不一样的。
数据结构是指数据以什么方式组织在一起。不同的数据结构,增删查的性能是不一样的。
栈:stack,又称堆栈,它是运算受限的线性表,只能在栈的受限一端进行插入和删除操作。
特点:先进后出
队列:queue,简称队,它同栈由于也是运算受限的线性表,只能在表的一端进行插入操作,而在表的另一端进行删除操作。
特点:先进先出
数组:Array,是个有序的元素序列,数组在内存中开辟一段连续的空间。
特点:
链表:linked list,由一系列结点node组成,结点可以在运行时动态产生。每个节点包含两个部分:数据域(data)和指向下一个节点的指针域(next)。链表包括单链表和双向链表。
特点:
红黑树:R-B Tree,全称是Red-Black Tree,又称为“红黑树”,它一种特殊的二叉查找树。红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black),它是一种弱平衡二叉树(Weak AVL)。
特点:
(1)每个节点或者是黑色,或者是红色。 (2)根节点是黑色。 (3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!] (4)如果一个节点是红色的,则它的子节点必须是黑色的。 (5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
注:以上数据结构可以结合所学过c语言数据结构
List集合体系:添加元素,是有序,可重复,有索引的,大小可变。实际开发中常用的是ArrayList集合。List集合体系包括以下几种:
List集合继承了Collection集合的全部功能,同时因为List集合系列有索引,所以多了很多按照索引操作元素的方法:
add(int index, E element) 根据索引添加元素
get(int index) 根据索引获取元素
remove(int index) 根据索引删除元素
set(int index, E element) 根据索引修改该位置上的元素
contains(E element)判断容器是否含有XXX东西
clear() 清空集合中的元素
size()计算集合的大小
【参考代码】
package Collection; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; public class TestList { public static void main(String[] args) { List<String>list=new ArrayList(); // 换成Linkedist 下面的操作都能一样实现 list.add("小明"); list.add("小红"); list.add("小蓝"); list.add("小绿"); list.add("小明"); // // 在某个索引位置往集合中添加元素 // list.add(2,"哈哈哈哈"); // System.out.println(list); // // 删除集合中某个元素 // list.remove("小蓝"); // // 根据索引获取元素 // System.out.println(list.get(0)); // // 修改索引位置处的元素 // list.set(0,"小明很明白!"); // System.out.println(list.get(0));//小明很明白! // // 计算列表的大小(长度): // System.out.println(list.size()); // //判断列表中是否有xxx false // System.out.println(list.contains("小蓝")); } }
ArrayList集合存储的结构是数组结构,元素增删慢,查询快。最常用。
LinkedList集合存储的结构是链表结构,方便元素的添加、删除操作。LinkedList是一个双向链表
LinkedList的特点:
实际开发中对一个集合的添加、删除操作经常涉及首尾操作,LinkedList提供了很多操作首尾元素方法
public void addFirst(E e); 将指定的元素插到列表开头。 public void addLat(E e); 将指定的元素插到列表结尾。 此方法等效于add()方法 public void push(E e); 将元素推入此列表所示的堆栈。 此方法等效于addFirst()方法 public E getFirst(); 返回此列表的第一个元素 public E getLast(); 返回此列表的最后一个元素 public E removeFirst(); 移除并返回此列表的第一个元素 public E removeLast(); 移除并返回此列表的最后一个元素 public E pop(E e); 入此列表所示的堆栈中弹出一个元素。 public boolean isEmpty(); 如果列表为空 返回true
【参考代码】
package Collection; /* public void addFirst(E e); 将指定的元素插到列表开头。 public void addLast(E e); 将指定的元素插到列表结尾。 public void push(E e); 将元素推入此列表所示的堆栈。 public E getFrist(); 返回此列表的第一个元素 public E getLast(); 返回此列表的最后一个元素 public E removeFrist(); 移除并返回此列表的第一个元素 public E removeLast(); 移除并返回此列表的最后一个元素 public E pop(E e); 入此列表所示的堆栈中弹出一个元素。 public boolean isEmpty(); 如果列表为空 返回true */ import java.util.LinkedList; import java.util.List; public class TestLinkedList { public static void main(String[] args) { show01(); show02(); show03(); } /* public void addFirst(E e); 将指定的元素插到列表开头。 public void addLast(E e); 将指定的元素插到列表结尾。 public void push(E e); 将元素推入此列表所示的堆栈 */ public static void show01(){ // 注:LinkedList特有的方法不能使用多态! // List<String> list=new LinkedList<>(); 是不对的 LinkedList<String>list=new LinkedList<>(); // add()添加元素 list.add("a"); list.add("b"); list.add("c"); System.out.println(list);//[a, b, c] list.addFirst("hhh"); //public void push(E e); 将元素推入此列表所示的堆栈。 等效于addFirst() list.push("hhh"); System.out.println(list); //public void lastFrist(E e); 将指定的元素插到列表结尾。 等效于add() list.addLast("com"); System.out.println(list); } /* public E getFrist(); 返回此列表的第一个元素 public E getLast(); 返回此列表的最后一个元素 */ public static void show02(){ LinkedList<String>list=new LinkedList<>(); // add()添加元素 list.add("a"); list.add("b"); list.add("c"); // list.clear(); // 清空集合中所有元素 if(! list.isEmpty()){ System.out.println(list.getFirst());//a System.out.println(list.getLast());//c } } /* public E removeFrist(); 移除并返回此列表的第一个元素 public E removeLast(); 移除并返回此列表的最后一个元素 public E pop(E e); 入此列表所示的堆栈中弹出一个元素。 */ public static void show03(){ LinkedList<String>list=new LinkedList<>(); // add()添加元素 list.add("a"); list.add("b"); list.add("c"); System.out.println(list.pop()); //public E pop(E e); 入此列表所示的堆栈中弹出一个元素。 等效于 removefirst() //System.out.println(list.pop()); System.out.println(list.removeFirst());//a System.out.println(list.removeLast());//c System.out.println(list);//[b] } }
注:使用LinkedList集合特有的方法,不能使用多态。
数组结构实现,查询快,增删慢;
JDK1.0版本,运行效率慢、线程安全
【参考代码】
package Collection; import javax.swing.text.html.HTMLDocument; import java.util.Enumeration; import java.util.Iterator; import java.util.Vector; /* Vector集合的使用 存储结构:数组 */ public class VectorTest { public static void main(String[] args) { // 创建集合 Vector<String>vector=new Vector<>(); // 添加元素 vector.add("小明"); vector.add("小红"); vector.add("小蓝"); System.out.println("元素个数"+ vector.size()); // 判断 System.out.println(vector.contains("小明")); System.out.println(vector.isEmpty()); //删除 vector.remove("小红"); System.out.println(vector); //清空 clear(); vector.clear(); // 遍历 Iterator<String> it=vector.iterator(); while (it.hasNext()){ String str=it.next(); System.out.println(str); } //vector独有的遍历 使用枚举器 // Enumeration<String>en=vector.elements(); // while (en.hasMoreElements()){ // String s=en.nextElement(); // System.out.println(s); // } } }
Set系列集合:添加的元素,是无序的,不重复的,无索引的(索引的操作不能用)。
——HashSet:添加的元素,是无序的,不重复的,无索引的。
——LinkedHashSet:添加的元素,是有序的,不重复的,无索引的。
——TreeSet:不重复,无索引,按照大小默认升序排序!!(可排序集合)
遍历方式:由于Set集合五索引,故没有for循环遍历,只有三种遍历。
注:存储的字符串,Integer等类型的数据,它们是Java已经定义好了类,它们都重写了hashCode方法和equals方法,保证了元素的唯一性!
HashSet 保证元素唯一性的原理
我们使用 Set 集合都是需要去掉重复元素的, 如果在存储的时候逐个 equals() 比较, 效率较低,哈希算法提高了去重复的效率, 降低了使用 equals() 方法的次数。
当 HashSet 调用 add() 方法存储对象的时候, 先调用对象的 hashCode() 方法得到一个哈希值, 然后在集合中查找是否有哈希值相同的对象,如果没有哈希值相同的对象就直接存入集合。如果有哈希值相同的对象, 就和哈希值相同的对象逐个进行 equals() 比较,比较结果为 false 就存入, true 则不存。存储元素必需要重写HashCode方法和equals方法
给HashSet中存放自定义的类型时,必需要重写HashCode方法和equals方法,建立自己的比较方式,才能保证HashSet集合中对象的唯一性!
【参考代码】
Person类:
package Collection; import java.util.Objects; public class Person { private String name; private int age; public Person(String name, int age) { this.name=name; this.age=age; } // 用于打印 @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } // 重写hashCode方法和equals方法 @Override public boolean equals(Object o) { if (this==o) return true; if (o==null || getClass() !=o.getClass()) return false; Person person=(Person) o; return age==person.age && Objects.equals(name, person.name); } @Override public int hashCode() { return Objects.hash(name, age); } public String getName() { return name; } public void setName(String name) { this.name=name; } public int getAge() { return age; } public void setAge(int age) { this.age=age; } }
// 主控制台 package Collection; import java.util.HashSet; import java.util.Set; /* HashSet存储自定义类型的元素 Set集合保证元素唯一性: 存储的元素(String Integer,...Student,Person...) 必须重写hashCode方法和equals方法 要求: 同名且同年龄视为同一个人噢 */ public class TestHaxhSet { public static void main(String[] args) { // 创建hashSet集合存储Person Set<Person>set=new HashSet<>(); //集合类存放对象的! // 创建对象(人) /* // 在没有重写hashCode方法和equals方法前,它们的哈希值都是不一样的,equals也为false 故没有重复 Person p1=new Person("小明",18); Person p2=new Person("小明",19); Person p3=new Person("小明",18); System.out.println(p1.hashCode());// 460141958 System.out.println(p2.hashCode());// 1163157884 System.out.println(p3.hashCode());// 1956725890 System.out.println(p1.equals(p2));// false set.add(p1); set.add(p2); set.add(p3); System.out.println(set);// [Person{name='小明', age=18}, Person{name='小明', age=19}, Person{name='小明', age=18}] */ // 重写hashCode方法和equals方法之后set对象就唯一性了 Person p1=new Person("小明",18); Person p2=new Person("小明",19); Person p3=new Person("小明",18); set.add(p1); set.add(p2); set.add(p3); System.out.println(set);// [Person{name='小明', age=19}, Person{name='小明', age=18}] } }
我们知道HashSet保证元素的唯一性,但存放进去的元素是无序的,那我们要保证有序,该怎么办好呢?
在HashSet下面的一个子类Java.util.LinkedHashSet。它是链表和哈希表组合的一个数据结构。
LinkedHashSet集合的特点:
底层是一个哈希表(数组+链表/红黑树)+链表:多了一条链表(记录元素的存储顺序),保证元素有序
具有可预知迭代顺序的 Set 接口的哈希表和链接列表实现,即按照将元素插入到 set 中的顺序(插入顺序)进行迭代。
HashSet与LinkedHashSet的区别:
【参考代码】
package Collection; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Set; public class TestHashSet { public static void main(String[] args) { Set<String>set=new HashSet<>(); set.add("kkk"); set.add("abc"); set.add("abc"); set.add("afterglow"); System.out.println(set);//[afterglow, abc, kkk] 无序,不重复 Set<String>Linkset=new LinkedHashSet<>(); Linkset.add("kkk"); Linkset.add("abc"); Linkset.add("abc"); Linkset.add("afterglow"); System.out.println(Linkset);//[kkk, abc, afterglow] 有序,不重复 } }
使用前提:
如果我们定义一个方法需要接收多个参数,并且多个参数类型一致,我们可以对其做如下格式的简化:
修饰符 返回值类型 方法名(参数类型... 形参名){ }
这个写法完全等价于:
修饰符 返回值类型 方法名(参数类型[] 形参名){ } ,
后者在调用时必须传递数组,而前者可以直接传递数据类型。
可变参数原理:
可变参数底层是一个数组,根据参数个数不同,会创建不同长度的数组来存储这些参数。传递参数的个数,可以是0~n个
【参考代码】
package Collection; public class KeBiancanShu { public static void main(String[] args) { int i=add(1,2,3,4); System.out.println(i); } // // 两个数的和 // public static int add(int a, int b){ // return a + b; // } // // 三个数的和,要是多个一直往下写,很麻烦! // public static int add(int a, int b, int c){ // return a + b +c; // } /* 求0~n个整数的和 数据类型已经确定:int 参数个数不确定,可以使用可变参数 */ public static int add(int...arr){ // System.out.println(arr);// [I@1b6d3586 底层是一个数组 // System.out.println(arr.length);// 可变数组的长度,却决于你添加的个数 int sum=0; for (int i : arr){ sum +=i; } return sum; } }
注意事项:
【示例代码】
/*
可变参数注意事项:
一个方法的参数列表,只能有一个可变参数
如果方法的参数有多个,那么可变参数必须写在参数列表的末尾!
*/
//一个方法的参数列表,只能有一个可变参数
// public static void method01(int...a,String...b){ 报错!
// }
//如果方法的参数有多个,那么可变参数必须写在参数列表的末尾!
public static void method02(String b, double c, int...a){
}
【参考代码】
public class Test { public static void main(String[] args) { List<Integer>list=new ArrayList<Integer>(); list.add(120); list.add(20); list.add(220); // 求最值 Integer max=Collections.max(list); System.out.println(max); Integer min=Collections.min(list); System.out.println(min); // 排序 Collections.sort(list); System.out.println(list); // 打乱顺序 Collections.shuffle(list); // 斗地主发牌 System.out.println(list); // 不定参数添加 Collections.addAll(list,456,789); System.out.println(list);//[220, 20, 120, 456, 789] } }
注意:
sort(List<T> list)使用前提:
排序的集合里边存储的元素,必须实现Comparable接口,重写接口中的方法compareTo定义排序的规则。在Java中Integer、String等等数据类型已经帮我们实现Comparable接口并重写接口中的方法compareTo了。如果要对自己定义的类进行排序,我们就要自己实现接口并重写compareTo然后进行自定义排序规则。
Comparable接口的排序规则:
自己(this) - 参数:升序,反之降序
【示例参考】:比较自定义类型
输出结果:
[Student{name='小明', age=18}, Student{name='小红', age=20}, Student{name='小蓝', age=19}] [Student{name='小明', age=18}, Student{name='小蓝', age=19}, Student{name='小红', age=20}]
sort(List< T > list , Comparator<? super T >)的使用:
Comparator:相当于找一个第三放的裁判,按照Comparator比较器里面重写的compare方法对元素进行排序比较
Comparator的比较规则:
o1 - o2 升序
【参考代码】
public class TestComparator { public static void main(String[] args) { List<Integer> list=new ArrayList<>(); list.add(2); list.add(1); list.add(3); Collections.sort(list, new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o1 - o2;// 升序 // return o2 - o1;// 降序 } }); System.out.println(list);// [1, 2, 3] } }
【示例参考】:比较自定义类型
Map集合的特点
注:映射由Map<K,V>接口的实例表示,它不是继承自Collection接口。
Map系列集合,常用子类的包括:
——HashMap
——LinkedHashMap
【HashMap集合】
java.util.HashMap<k , v >集合implements Map<k , v>接口.
HashMap集合的特点:
【LinkedHashMap集合】
java.util.LinkedHashMap<k , v >集合extends HashMap<k , v>集合。
LinkedHashMap集合的特点:
Map接口中定义了很多方法,常见如下:
【参考代码】
package Map; import java.util.HashMap; import java.util.Map; public class Test { public static void main(String[] args) { // 创建Map集合对象,多态 Map<Integer,String>map=new HashMap(); map.put(11,"小明"); map.put(22,"小红"); map.put(33,"小蓝"); map.put(44,"小绿"); System.out.println(map);// {33=小蓝, 22=小红, 11=小明, 44=小绿} HashMap无序的 map.remove(44);// 删除 System.out.println(map);// {33=小蓝, 22=小红, 11=小明} System.out.println(map.size()); //大小 3 System.out.println(map.containsKey(33)); //true System.out.println(map.containsValue("小蓝")); //true map.put(22,"小芳"); // {33=小蓝, 22=小芳, 11=小明} 若出现重复的key原来的数据会被顶替 System.out.println(map); // map.put(55,"小明"); // System.out.println(map);//是否被顶替却决于key,key映射value,而不是value映射key {33=小蓝, 22=小芳, 55=小明, 11=小明} System.out.println(map.keySet()); // [33, 22, 11] 把map中的key打包成Set集合的形式 System.out.println(map.get(33));// 小蓝 通过key查询value } }
方法一:通过键找值的方式
【参考代码】
package Map; import javax.swing.text.html.HTMLDocument; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; public class Test { public static void main(String[] args) { // 创建Map集合对象 Map<String,Integer>map=new HashMap<>(); map.put("小明",18); map.put("小红",18); map.put("小蓝",19); map.put("小绿",20); //1. 使用Map集合中的方法keySet(),把Map集合里所有的key取出来,存放到一个Set集合中\ Set<String> set=map.keySet(); //2.遍历set集合,获取Map集合中的每一个key /* 使用while遍历 */ Iterator <String> it=set.iterator(); while (it.hasNext()){ String key=it.next(); //3.通过Map集合中的get(key)方法,找到value Integer value=map.get(key); System.out.println(key+"="+value); } System.out.println("-----------------------"); /* 使用增强for遍历 */ for(String key : set){ //3.通过Map集合中的get(key)方法,找到value Integer value=map.get(key); System.out.println(key+"="+value); } } }
【总结】:
while——迭代器遍历:
Set<String> set=map.keySet();
Iterator <String> it=set.iterator();
while (it.hasNext()){
String key=it.next();
Integer value=map.get(key);
System.out.println(key+"="+value);
}
增强for遍历:
Set<String> set=map.keySet();
for(String key : set){
//3.通过Map集合中的get(key)方法,找到value
Integer value=map.get(key);
System.out.println(key+"="+value);
}
方法二:键值对的方式遍历(更加面向对象)
把键值对当成一个整体遍历,增强for无法遍历,这个整体不是类型,因此Java提供了方法:
Map集合通过代码Set<Map.Entry<K,V>> ,将键值对元素转成了一个实体类型,此时得到的是一个Entry对象,类型是:Map.Entry<K,V>
【参考代码】
package Map; import javax.swing.text.html.HTMLDocument; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; public class Test { public static void main(String[] args) { // 创建Map集合对象 Map<String,Integer>map=new HashMap<>(); map.put("小明",18); map.put("小红",18); map.put("小蓝",19); map.put("小绿",20); //1.通过Map集合中的entrySet()方法,把Map集合中的多个Entry对象取出来,存储到一个Set集合中 Set<Map.Entry<String,Integer>> set=map.entrySet(); //遍历set集合,获取每一个Entry对象 //使用迭代器遍历set集合 Iterator <Map.Entry<String,Integer>> it=set.iterator(); while (it.hasNext()){ Map.Entry<String,Integer>entry=it.next(); // 使用Entry对象中的getKey()和getValue()方法获取键和值 String key=entry.getKey(); Integer value=entry.getValue(); System.out.println(key+"="+value); } System.out.println("-----------"); //增强for for(Map.Entry<String,Integer> entry : set){ // 使用Entry对象中的getKey()和getValue()方法获取键和值 String key=entry.getKey(); Integer value=entry.getValue(); System.out.println(key+"="+value); } } }
【总结】:
while——迭代器遍历:
Set<Map.Entry<String,Integer>> set=map.entrySet(); //遍历set集合,获取每一个Entry对象 //使用迭代器遍历set集合 Iterator <Map.Entry<String,Integer>> it=set.iterator(); while (it.hasNext()){ Map.Entry<String,Integer>entry=it.next(); // 使用Entry对象中的getKey()和getValue()方法获取键和值 String key=entry.getKey(); Integer value=entry.getValue(); System.out.println(key+"="+value); }
增强for遍历:
//增强for
for(Map.Entry<String,Integer> entry : set){
// 使用Entry对象中的getKey()和getValue()方法获取键和值
String key=entry.getKey();
Integer value=entry.getValue();
System.out.println(key+"="+value);
}
Entry:表示一个key和value,它提供了获取对应key和value的方法:
public K getKey():获取Entry中的key
public V getValue():获取Entry中的value
方法二图解:
练习:每位学生(姓名,年龄)都有自己的家庭住址。那么,既然有对应关系,则将学生对象和家庭住址存储到map集合中,学生作为键,地址为值。
注:学生姓名、年龄相同则视为同一人
package Map; /* hashMap存储自定义类型键值: Map集合保证key是唯一的: 作为key元素,必须重写hashMap方法和equals方法,以保证key唯一 */ import java.util.HashMap; import java.util.Set; public class HashMapSavePerson { public static void main(String[] args) { show01(); /* 上海-->HashMapSavePerson{name='小蓝', age=18} 深圳-->HashMapSavePerson{name='小绿', age=18} 北京-->HashMapSavePerson{name='小红', age=18} key唯一 */ } /* hashMap存储自定义类型键值: key:String类型 String类重写hashCode方法和equals方法,可以保证key唯一 value:Person类型 value可以重复(同名同年龄视为重复) */ public static void show01(){ // 创造HashMap集合 HashMap<String,Person> map=new HashMap<>(); //往集合中添加元素 map.put("深圳",new Person("小明",18)); map.put("上海",new Person("小蓝",18)); map.put("北京",new Person("小红",18)); map.put("深圳",new Person("小绿",18)); // 使用keySet()增强for遍历map集合 Set<String> set=map.keySet(); for(String key:set){ Person value=map.get(key); System.out.println(key+"-->"+value); // 因为字符串类(Java帮我们的)重写了hashCode方法和equals方法,所以键(key)是不能重复的 } } }
Person类:
下面这个是我们自己定义的key的类型,Person类,上面例子的是String类:
package Map; /* hashMap存储自定义类型键值: Map集合保证key是唯一的: 作为key元素,必须重写hashMap方法和equals方法,以保证key唯一 */ import javax.swing.text.html.HTMLDocument; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; public class HashMapSavePerson { public static void main(String[] args) { show02(); } /* hashMap存储自定义类型键值: key:Person类型 Person就必须类重写hashCode方法和equals方法,来保证key唯一 value:String类型 value可以重复(同名同年龄视为重复) */ public static void show02(){ // 创造HashMap集合 HashMap<Person,String> map02=new HashMap<>(); // 往集合中添加元素 map02.put(new Person("张三",18),"法外狂徒"); map02.put(new Person("黄老板",18),"英国"); map02.put(new Person("陈奕迅",18),"中国"); map02.put(new Person("张三",18),"法外狂徒"); // 使用迭代器遍历set集合中的Entry对象 Set<Map.Entry<Person,String>> set=map02.entrySet(); Iterator<Map.Entry<Person,String>> it=set.iterator(); while(it.hasNext()){ Map.Entry<Person,String> entry=it.next(); Person key=entry.getKey(); String value=entry.getValue(); System.out.println(key+"--->"+value); } } }
这里再介绍一下本例中Entry对象遍历的图解,再次加深印象:
我们知道HashMap保证key唯一,并且查询速度快,可是成对元素存放进去是没有顺序的(存和取的顺序可能不一致),那我们要如何保证顺序呢?
在HashMap下面有个LinkedHashMap(继承关系),它是链表(记录元素的顺序)和哈希表组合的一个数据存储结构,是个有序的集合
【参考代码】
package Map; import javax.swing.text.html.HTMLDocument; import java.util.*; public class Test { public static void main(String[] args) { HashMap<String,String> map=new LinkedHashMap<>(); map.put("a","a"); map.put("c","c"); map.put("b","b"); map.put("d","d"); System.out.println(map);//{a=a, c=c, b=b, d=d} } }
输出结果:(存储和取出的顺序是一样的)
{a=a, c=c, b=b, d=d}
看到这里,相信各位小伙伴们对Java集合这一章节的知识有了进一步的理解,尤其是一些在之前学习时可能没有注意到的知识或者原理,没关系,这次都帮你总结在一起了。最后,感谢看到这里的你!愿你韶华不负,青春无悔!
原文:https://www.cnblogs.com/lwtyyds/p/15167601.html
者 | kakashi8841
责编 | 屠敏
出品 | CSDN 博客
简述
这篇文章并不是想教会大家如何开发游戏,更不能教大家如何成为技术总监。而是分享我一路做开发的一些经历或心得体验。
与编程擦肩而过
2004年,初三升高中时,因为我考上市里最好的高中,因此父母兑现承诺给我买了一台电脑。那时候电脑配置还是挺好的(我记得是P4的CPU、1G内存、80G硬盘、优派的液晶显示器,还买了打印机、扫描仪)。
由于我热爱游戏,因此有了电脑之后就开始陷入于各类游戏中无法自拔。基本每天晚上通宵玩游戏,玩到凌晨5、6点睡1-2个小时,然后去上学。基本高一玩了各种游戏,然后成绩自然也是一落千丈。
到了高二的时候,因为我表姐夫是做美术设计的,因此我开始接触了Photoshop、Coredraw这两个软件。我一边瞎画着各种东西,一边想如果能让这些也动起来多好。因此开始接触了Flash。开始在网上看看各种Flash的教程,能做一些动画,能用按钮控件控制一些流程,也了解到AS2这一门语言,然后在网上找了各种酷炫的AS2代码加到自己的Flash里面,实现比如鼠标跟随的特效等等效果。这时候其实我还不会独立地写代码。
由于高中各种的不务正业。从中考数学全市前几名,到最后高考没考上重点大学,这其实也是我预料中的。因为本来我就不是一个相信运气的人。那时候我只想快点脱离高中,可以更自由做自己喜欢做的事情。因此也没有选择复读。
从图像设计到痴迷编程
入门编程
进入大学之后,我参与了好几个社团,成为各个社团网络部的成员。然后接下来的事情就是社团搞活动经常需要一些海报什么的。因此我高中自学的Photoshop和Coredraw就派上用场了。大一基本就是做了一年各种图像设计的工作。而从高中到大一带来的各种突如其来的自由,也使得我继续沉浸在各种游戏中。到了大一的暑假时,校团委突然找网络部帮忙做网站,而师兄们准备毕业的、准备考研的,都没啥时间。因此这重任落到了我一个人头上,我也不知道那么多成员为啥选中了我,也没想那么多。
我暑假放假前1个月,外加暑假2个月,整个3个月时间开始疯狂学习HTML、CSS、Javascript、PHP、MySQL,然后3个月时间从学习到开发完成了校团委的任务,制作了一个CMS网站。这三个月时间让我觉得编程原来这么有趣。
沉迷编程
接下来大二的时候我就陷入了疯狂的学习和开发之中,最疯狂的时候一天在图书馆借一本PHP的书,当天借当天看完,晚上再自己把那本书最后的例子实现了,然后第二天再去换书,就这样我很快把图书馆所有PHP的书都看完了。书中的例子也都能自己写出来。然后开始接了很多外包,也快速赚到了第一个笔记本的钱。
Java的图形编程
由于大二做了很多的网站,已经觉得有点厌倦。进入大三,刚好我们有一门Java的课程,我看到Java可以开发图形界面(Swing/Awt),因此开始转向学习Java。我大概花了2-3周把学校发的Java书看完了,并且把里面的示例也都自己敲了一遍。基本没有障碍的完成了Java的学习。但是书本里面关于图形界面的太少了,只介绍了最基本的一些控件以及做一些简单的界面。因此我开始自己在网上找各种资料。这时候室友告诉我“Java做图形不好看的,还不如C++不如C#,等等”。我就偏偏不信了,我依旧进行自己的学习和研究,而他也依旧站在他认为的鄙视链顶端鄙视我。就这样,他学习他的SSH,而我学习着不入流的Swing、Java3D等。
其实我那时候的心理:为啥你觉得他不行他就不行?我得自己实践过才知道。实践出真知,我觉得这是做技术一个很重要的特质。很多时候,很多东西大家只是凭经验人云亦云地传播,并没有亲自实践过。而我那时候还有一个心理,哪怕最后我花了很多时间之后做出来的东西确实不好看,那我也不亏,我在这个过程中肯定是能学到东西的。很多时候很多人会比较看重眼前做的事情收益如何,回收周期多长。但是我觉得,很多值得学习的知识都是回收周期很长,而且眼前收益很低的。对于我而言,我更多的是基于兴趣去学习。我觉得这就足够了。
最后的结果就是,我用Java开发出来的东西,他们觉得太好看,根本难以想象是Java开发的。这里有一些大学用Java开发的图形程序。
1. swing超绚丽系列一—— 俄罗斯方块(https://kakashi.blog.csdn.net/article/details/7338836)
2. 纯Java开发的游戏引擎V0.5–DEMO2 – 物理引擎(https://kakashi.blog.csdn.net/article/details/6397051)
3. 杂七杂八的东西(https://kakashi.blog.csdn.net/article/details/6181486)
就职游戏开发
2011,第一次面试&实习生
2011年初,不知不觉到了大四第二学期。本来我还不想这时候去找工作,因为此时我觉得还有很多事情想做,比如那时候我还在用Java自己做一个3D游戏。
但是大四寒假的时候,有个同学发了一个网页游戏公司的招聘链接给我,说这个公司还不错,我看了一下招聘要求,感觉自己符合要求,于是就顺手投了个简历。
其实投简历之前,那时候北京GLU有位前辈想让我毕业后去试试。还有另一家互联网公司也是让我毕业后去工作。主要在于我大学期间经常在各个技术QQ群里帮大家解决问题,最开始他们以为我应该工作了几年,后来知道我还在上学,因此就想我毕业后去他们公司工作。
后来肯定是我投的简历的公司让我去面试了,那天从下午2点面试到下午6点。从HR、主程序、制作人到CTO都面了。CTO面试的时候网络的BIO、NIO、多线程的安全问题如何解决也都问了。最后CTO说“你简历里写的都是Java做的图形界面,我们这Java都是做服务器,你接受做服务器吗“,我怕服务器又是做网站,于是问他”服务器也是做游戏吗“,他说“肯定是做游戏。我们做的是网游”。因此我就说“可以呀,只要是做游戏就好。”于是CTO接着说“你进我们公司肯定是没问题的,估计过几天会给你发Offer,你也不用去面试别的公司了”。
因为我个人也懒得比较,既然CTO也那么说了,我就没去面试别的公司,在同学那玩了一周,一周后就入职了。
记得那时候CTO还问过我一个问题“你说你这么喜欢写代码, 你想写到几岁”,我说“30岁吧”。他问我为啥只写到30岁。我说那时候可能我是技术总监了。就不用写代码了。后来我30岁的时候,成为了技术总监。但是我还是喜欢写代码。其实兴趣是可以一辈子的。我也不知道为啥面试那时候那么回答了。
2011,第一个游戏
刚进入工作比较紧张,总怕哪里做不好。第一周项目组说让我去改一下服务器列表。我那时候压根没有远程登陆过服务器改东西,主程就给了我IP、用户名、密码,以及网页的位置。于是我赶紧自己搜索了各种资料,在接到任务的半小时内,我第一次用SecureCRT登陆服务器,第一次用VI在服务器上修改文件。
接着几天就是熟悉项目,我在服务器上自己找到了聊天的协议入口,然后加入了一些GM,给自己的账号加了赵云、吕布这些牛逼的武将。
几天后,主程找我,说来的毕业生里面感觉你编程能力不错,你想去做新项目吗。我自然想体验从0开发游戏的过程。于是就满心雀跃地说“想!”。
新项目开发的时候我充满了激情,基本上第一周我就完成了背包系统的开发。那时候公司其他游戏的数据存储都是同步的。主程就说,这个新项目估计用户交互会比较频繁和实时,同步存储怕会卡顿。于是我那一周开发背包的时候,顺便被服务器的数据存储写了个异步的存储。(后面该功能还被公司其他那项目组引用),立项一个月后主程就去了腾讯。而我们项目本来是公司的尝试性项目(公司其他项目都是SLG,而这个项目是RPG),因此也一直得不到重视,一直得不到重视。我们这项目加上我就2个服务器,开发了半年上线了。我对游戏业务非常熟,编程基础也还行,因此开发速度很亏,一个人开发了70%的系统。那时候很多同学问我,你做那么多,公司给你加工资了没有。我说没有,他们说,那没有你为啥做那么多。
其实有的时候不是你在这个位置才能做这个事情。而是你有能力、主动地去做这个位置的事情,做多了你自然也就是这个位置的人。因为我觉得没有哪个公司会愿意付出成本让你冒险。
项目上线的时候,公司也让我成为了这个项目的服务器主程。那时候我距离实习才半年,一直怕自己做不好,因此一直很努力提升各种能力。由于我开发的功能Bug极少,而且出Bug的时候修改极快,因此项目组的测试负责人说我说最受测试欢迎的程序,直到后来她去了银汉后遇到她,她还说她在银汉和测试部说,曾经有个程序员这样这样…
写程序出BUG是常见的事情,想要减少BUG数量,要思维谨慎,业务熟悉,而想要修BUG快速,需要对所写的东西都烂熟于心,我之前经常对别人说要做到代码在心中。
如果你写的代码你内心都很清晰,那么出问题的时候,很快就能定位到问题。这就像,你的代码存在于内存中,查找肯定快。如果每次都是要去慢慢看代码文件,那就像每次都检索磁盘,肯定会慢一点。我之前好多个性能问题都是在走路和洗澡想到的解决方案,假如心中没有代码,又如何能做到呢?
2012年,第二个重量级IP游戏
12年初的时候,公司准备做一个ARPG项目,是星爷授权合作的游戏《西游降魔篇》,那时候公司从我们第一个项目的表现觉得我们项目组还是具备比较好的研发能力的,因此这个项目就给了我们项目组。
一开始公司让我做这个项目的主程序,我觉得我才毕业1年,我玩游戏很多,自知ARPG项目比我上一个RPG肯定要高。因此最开始希望公司能招一个经验丰富的主程,我可以过去学习。公司也同意了,主程入职了一直在看天堂2的源码,也拉着我看。我觉得天堂2的源码存在很多问题。比如各种在“父类中判断自己是否是某个子类”等。后来2个月过去了,连登陆都还不能登陆。因此制作人就问我,说你觉得他行不行。我也把我对天堂源码的判断说了一下。
后来制作人说,要不还是你来当主程吧。那时候我觉得也行吧。反正本来想跟一个有经验的人学东西,结果反而他老来问我各种东西,而且开发进度也很慢。于是我就答应了。然后我又重新开始搭框架。第一周花了时间把网络、副本采用的线程模型搭好了。第二周CTO带着我做了场景。第二周我们已经可以多客户端同步移动、怪物有了简单的AI和普攻了。而且我为了减少和客户端连调的不便,还自己用Swing写了一个图形界面,用于显示服务器当前地图中各个角色的位置。(所以说,假如我大学的时候很功利地觉得学习Java的图形开发性价比不高,那么这时候我肯定没法自己快速完成这个工具)
这个项目我们花了9个月上线。作为公司第一个ARPG项目,其实也很快了。
项目上线后玩的人挺多,那时候单物理服导量的时候有6000人同时在线。不过这个游戏收费没有调起来。
2013年,转战手游与创业
西游降魔篇调了整整8个月都没把付费调起来,甚至越来越差。从最开始每月单服的700W变为几乎没有。此时已经是2013年中,我建议公司可以使用Unity3D(其实12年底已经说过一次,制作人说次年可以申请看看)开发手游,但是公司的战略明显还是在页游,因为公司靠页游已经做到借壳上市,不愿意也觉得没必要去踩手游的坑。公司唯一一个和手游有关的部门其实是把页游项目通过AIR发布为手游。这样出来的手游性能堪忧,而且体验也没有做针对性优化。
于是在2013年8月,我就辞职自己去创业做手游了。
自己创业肯定面临的是资金问题,短缺的资金不可能请得了很多牛人,因此我自己又开始做起了Unity3D客户端。把战斗等最核心的东西都自己扛下来。
我们2策划、2程序、1美术从立项到做完Demo花了大概3个月,然后开始拿着手机去演示DEMO,去找投资。
那时候的DEMO是用Unity3D做了一个2D游戏。因为我们唯一的美术人员比较擅长画画。Demo可以在这里看到:第一个Unity3D 的Demo(https://kakashi.blog.csdn.net/article/details/29365329)。
我们大概找了4个投资人谈,最后确定了投资。
2014-2015,第一次创业
就像上面说的,创业资金的短缺,而且那时候Unity3D人员的稀缺,导致我们想找一个牛逼的人员根本不可能,因此我只能自己扛下战斗、Shader等核心的工作。那时候白天写服务器,教新人写代码,一行行代码地边敲边讲,晚上写客户端,最疯狂的几个月基本睡公司,凌晨5、6点入睡,早上9点半起来。
那时候我用Unity3D,也只是为了为公司省成本,也没想什么太多的东西。结果也正是这段经历,使得我后来成为了另一家百人公司的技术总监。所以我还是挺相信一句话:但行好事,莫问前程!
最后创业肯定还是失败了,其实还是因为团队成员的不成熟。因此在2015年中的时候,经过了几轮调整和测试后,由于测试结果表现不理想,我们就结束了创业。我并没有对这次创业感到后悔,毕竟那是一次宝贵的经历。而且整个创业过程也都是很充实,很开心。
2015,西山居和多益网络
创业结束后我觉得想找个公司积累一下。那时候只面试了珠海西山居和多益网络,西山居剑网3项目组也希望我过去,但我觉得我就是因为懒,最后选择了继续留在广州的多益网络。我学习可以很努力,但是对于公司的选择却经常是很懒。
2016-2018,第二次创业
在多益做的时间不长,基本过了试用期,以前第一家公司的制作人就找我创业。并不是因为我个人喜欢创业,其实我只想做一个成功的游戏,能被大家认可的游戏。那时候出去创业,更多是因为这位制作人以前对我不错,也是因为他我才能刚毕业就当上主程,因此这次创业,其实更多的还是报恩。
这次创业的结果无疑还是失败的。
2018至今,360游戏艺术(岂凡网络)技术总监
2018年4月,由于以前带的一位程序的推荐,我便去了360游戏艺术担任技术总监。这时候刚好30岁,正好和刚毕业实习的时候自己说30岁可能会当上技术总监。实现了自己当初不假思索的一句话。但是回想起来,我这一路并没有为当技术总监去做任何刻意的学习与准备。只是我一路以来学习的都是自己喜欢的,做的都是自己喜欢的,而且该公司正好需要我目前拥有的能力而已。这也是我最后想和大家分享的,有的人会问怎么才能当主程,怎么才能当总监。我觉得不管当什么职位,重要的就是为公司解决问题,你能解决什么样的问题,你自然就能胜任什么岗位。而有很多舍本逐末的做法,其实看似近路,最后都是无尽的弯路。我觉得做什么都不重要,重要的应该想想,自己能为游戏这个行业做什么,平常应该怎么提升自我去成为行业真正渴求的人。
当然我从任该岗位以来,一直也觉得自己还是做的不够好,也还在一直努力提升自己。
今天正好有空,谨以此文,献给所有想为自己所在行业做点贡献的小伙伴!希望一起努力,共勉!
声明:本文已获 CSDN 博主 kakashi8841 授权,版权归作者所有。
原文:https://blog.csdn.net/kakashi8841/article/details/100065038
【END】
*请认真填写需求信息,我们会在24小时内与您取得联系。