JDBC工具包使用
数据库结构图:
第一种组合 :
只有官方驱动 mysql-connector-java-5.1.13-bin.jar
包结构:
代码:简单实现增删改查可以自己做一个配置文件工具包,见上一篇文章
1 package com.jdbc.onlyconnector; 2 3 import java.sql.Connection; 4 import java.sql.DriverManager; 5 import java.sql.PreparedStatement; 6 import java.sql.ResultSet; 7 import java.sql.SQLException; 8 import java.util.Scanner; 9 10 import org.junit.Test; 11 12 public class ConnectorTest { 13 @Test 14 public void test01() throws ClassNotFoundException, SQLException { 15 //jdbc四大参数,driver的全限命名,url,user,password 16 String driverClassName = "com.mysql.jdbc.Driver"; 17 String url = "Jdbc:mysql://localhost:3306/db2"; 18 String user = "root"; 19 String password ="123"; 20 /* 21 * public class Driver implements java.sql.Driver 22 * 获取到Driver的对象(创建驱动),com.mysql.jdbc.Driver类的内部有一个静态代码块,当类被加载时就会调用 23 * java.sql.DriverManager.registerDriver(new Driver())这个方法 24 * 该驱动实现了java内部的Driver接口,通过多态传递给驱动管理,进行底层连接 25 */ 26 Class.forName(driverClassName); 27 /* 28 * 通过传递另外三大参数和数据库取得连接,并且返回一个Connection 对象, 29 * Connection对象的数据库能够提供描述其表、所支持的 SQL 语法、存储过程、此连接功能等等的信息 30 * Connection类主要用到的方法 commit 提交上一次修改; rollback 回滚到数据库(不修改) 31 * getAutoCommit() true为自动提交,false为手动事务,默认true 32 * createStatement() 创建一个 Statement 对象来将 SQL 语句发送到数据库,返回值就是一个Statement 对象 33 * prepareStatement(String sql) 创建一个 PreparedStatement 对象来将参数化的 SQL 语句发送到数据库。 34 * 注意:prepareStatement extends Statement 两个都是接口 Connection也是接口 35 */ 36 Connection connection = DriverManager.getConnection(url, user, password); 37 // /* 38 // * 第一种 :开始 39 // * 调用 createStatement() 方法返回一个Statement对象 40 // * Statement类用于执行静态 SQL 语句并返回它所生成结果的对象。 41 // * Statement类主要用到的方法 42 // * boolean execute(String sql)执行给定的 SQL 语句,该语句可能返回多个结果。 43 // * ResultSet executeQuery(String sql) 执行给定的 SQL 语句,该语句返回单个 ResultSet 对象 ;(查数据库) 44 // * int executeUpdate(String sql) 执行给定 SQL 语句, 45 // * 该语句可能为 INSERT、UPDATE 或 DELETE 语句,或者不返回任何内容的 SQL 语句(如 DDL 语句); (改数据库) 46 // */ 47 // Statement statement = connection.createStatement(); 48 // /* 49 // * 增 数据insert 50 // */ 51 // String zeng = "insert into emp(eid , ename ,did) values (4,'小猪',1)"; 52 // //返回值是表中受影响的行数 53 // int empZeng = statement.executeUpdate(zeng); //返回值是1 代表1行收到影响 54 // /* 55 // * 删 数据 delete 56 // */ 57 // String shan = "delete from emp where eid = 4"; 58 // int empShan = statement.executeUpdate(shan); 59 // /* 60 // * 改 数据update 61 // */ 62 // String gai = "UPDATE emp SET ename = '小红' WHERE eid = 3 "; 63 // int empGai = statement.executeUpdate(gai); 64 // /* 65 // * 重点:查 数据 select 66 // */ 67 // String cha = "select * from emp"; 68 // /* 69 // * 返回值是ResultSet类的对象,表示数据库结果集的数据表,通常通过执行查询数据库的语句生成。 70 // * ResultSet类常用方法 71 // * boolean next() 将光标从当前位置向前移一行。 (和迭代器差不多) 72 // * int getInt(int columnIndex) 获取索引列的int类型的值 columnIndex 第一列等于1 73 // * String getString(int columnIndex) 74 // * Object getObject(int columnIndex) 75 // * Object getObject(String columnLabel) 获取此 ResultSet对象中当前行中指定列的值 76 // * (columnLabel 指定的列标签。如果未指定 SQL AS 子句,则标签是列名称 ) 77 // * String getString(String columnLabel) 获取此 ResultSet 对象的当前行中指定列的值。 78 // * int getInt(String columnLabel) 79 // * 80 // */ 81 // ResultSet empCha = statement.executeQuery(cha); 82 // if(empCha!=null) { 83 // while(empCha.next()) { 84 // System.out.println(empCha.getInt(1)); 85 // System.out.println(empCha.getString("ename")); 86 // System.out.println(empCha.getObject(3)); 87 // } 88 // } 89 ///* 90 // 输出结果: 91 //1 92 //张三 93 //1 94 //2 95 //李四 96 //2 97 //3 98 //小红 99 //1 100 // */ 101 /* 102 * 第二种 使用preparedStatement 103 */ 104 /* 105 * 查数据更加方便 好处: 106 * 1.使用占位符而不是语句拼接,可以防止注入攻击; 107 * 2.语句预编译,执行效率高; 108 */ 109 String sql = "select * from emp where eid =?"; 110 Scanner sc = new Scanner(System.in); 111 System.out.println("请输入要查找的员工id"); 112 int eid =sc.nextInt(); 113 PreparedStatement ps = connection.prepareStatement(sql); 114 //setInt(int parameterIndex, int x) parameterIndex占位符下标,第一个是1 x代表占位符的值 115 ps.setInt(1, eid); 116 //执行语句 117 ResultSet rs = ps.executeQuery(); 118 if(!rs.wasNull()) { 119 while(rs.next()) { 120 System.out.println(rs.getObject(1)); 121 System.out.println(rs.getObject(2)); 122 System.out.println(rs.getObject(3)); 123 } 124 } 125 /* 126 执行结果: 127 请输入要查找的员工id 128 1 129 1 130 张三 131 1 132 */ 133 134 } 135 }
第二种组合 :
官方驱动 mysql-connector-java-5.1.13-bin.jar
C3p0连接池 和它的关联包 以及它的配置文件
c3p0-0.9.2-pre1.jar mchange-commons-0.2.jar c3p0-config.xml(配置文件放在src目录下)
包结构
代码:
<?xml version="1.0" encoding="UTF-8" ?> <c3p0-config> <default-config> <property name="jdbcUrl">jdbc:mysql://localhost:3306/db2</property> <property name="driverClass">com.mysql.jdbc.Driver</property> <property name="user">root</property> <property name="password">123</property> <property name="acquireIncrement">3</property> <property name="initialPoolSize">2</property> <property name="minPoolSize">2</property> <property name="maxPoolSize">20</property> </default-config> </c3p0-config>
1 package com.jdbc.usec3p0utils; 2 3 import java.sql.Connection; 4 import java.sql.SQLException; 5 6 import com.mchange.v2.c3p0.ComboPooledDataSource; 7 /* 8 * 通过c3p0jar包封装一个工具类,c3p0中有一个连接池类,通过创建连接池来获取到一个dateSource对象,通过调用这个 9 * 对象的getConnection方法获取一个connection连接对象,这就是这个工具类的作用 10 * 11 * 对c3p0的理解 ,就是通过c3p0-config.xml配置文件,管理对数据库的连接,实现随时取用,提高效率 12 * 13 */ 14 public class C3p0Utils { 15 //C3P0连接池提供的核心类: 数据源(就是一个名词,作用是可以调用他的方法给我们一个连接对象) 16 private static ComboPooledDataSource dataSource=new ComboPooledDataSource(); 17 18 //获取数据源 19 public static ComboPooledDataSource getDataSoruce() { 20 return dataSource; 21 } 22 //从连接池中取用一个连接 23 public static Connection getConnection() throws SQLException { 24 return dataSource.getConnection(); 25 } 26 27 }
1 package com.jdbc.usec3p0utils; 2 3 import java.sql.Connection; 4 import java.sql.PreparedStatement; 5 import java.sql.ResultSet; 6 import java.sql.SQLException; 7 8 import org.junit.Test; 9 10 public class C3p0Test { 11 @Test 12 public void selectByC3p0Utile() throws SQLException { 13 /* 14 * 使用 c3p0工具包获取到一个连接对象,在进行数据库的操作 15 */ 16 C3p0Utils c3p0Utils = new C3p0Utils(); 17 Connection connection = c3p0Utils.getConnection(); 18 //进行数据库操作 19 String sql = "select * from emp"; 20 PreparedStatement ps = connection.prepareStatement(sql); 21 ResultSet rs = ps.executeQuery(); 22 //输出查询结果 23 if(!rs.wasNull()) { 24 while(rs.next()) { 25 System.out.println(rs.getObject(1)); 26 System.out.println(rs.getObject(2)); 27 System.out.println(rs.getObject(3)); 28 } 29 } 30 /* 31 执行结果 32 1 33 张三 34 1 35 2 36 李四 37 2 38 3 39 小红 40 1*/ 41 } 42 }
第三种组合:
1.官方驱动 mysql-connector-java-5.1.13-bin.jar
2.C3p0连接池 和它的关联包 以及它的配置文件
c3p0-0.9.2-pre1.jar mchange-commons-0.2.jar c3p0-config.xml(配置文件放在src目录下)
3.commons-dbutils-1.4.jar
Dbutils:主要是封装了JDBC的代码,简化dao层的操作。
Dbutils作用:帮助java程序员,开发Dao层代码的简单框架。
框架的作用:帮助程序员,提高程序的开发效率。
出生:Dbutils是由Apache公司提供。
为什么需要Dbutils ?
在使用Dbutils 之前,我们Dao层使用的技术是JDBC,那么分析一下JDBC的弊端:
(1)数据库链接对象、sql语句操作对象,封装结果集对象,这三大对象会重复定义
(2)封装数据的代码重复,而且操作复杂,代码量大
(3)释放资源的代码重复
结果:(1)程序员在开发的时候,有大量的重复劳动。(2)开发的周期长,效率低
包结构:
代码 :将数据库数据转化成员工对象
<?xml version="1.0" encoding="UTF-8" ?> <c3p0-config> <default-config> <property name="jdbcUrl">jdbc:mysql://localhost:3306/db2</property> <property name="driverClass">com.mysql.jdbc.Driver</property> <property name="user">root</property> <property name="password">123</property> <property name="acquireIncrement">3</property> <property name="initialPoolSize">2</property> <property name="minPoolSize">2</property> <property name="maxPoolSize">20</property> </default-config> </c3p0-config>
1 package com.jdbc.usec3p0utils; 2 3 import java.sql.Connection; 4 import java.sql.SQLException; 5 6 import com.mchange.v2.c3p0.ComboPooledDataSource; 7 /* 8 * 通过c3p0jar包封装一个工具类,c3p0中有一个连接池类,通过创建连接池来获取到一个dateSource对象,通过调用这个 9 * 对象的getConnection方法获取一个connection连接对象,这就是这个工具类的作用 10 * 11 * 对c3p0的理解 ,就是通过c3p0-config.xml配置文件,管理对数据库的连接,实现随时取用,提高效率 12 * 13 */ 14 public class C3p0Utils { 15 //C3P0连接池提供的核心类: 数据源(就是一个名词,作用是可以调用他的方法给我们一个连接对象) 16 private static ComboPooledDataSource dataSource=new ComboPooledDataSource(); 17 18 //获取数据源 19 public static ComboPooledDataSource getDataSoruce() { 20 return dataSource; 21 } 22 //从连接池中取用一个连接 23 public static Connection getConnection() throws SQLException { 24 return dataSource.getConnection(); 25 } 26 27 }
1 package com.jdbc.entity; 2 3 public class Employee { 4 /* 5 * 创建对象时必须用数据库的字段作为属性,因为dbutils内部将查询结果转化成对象是 6 * 通过我们Employee类的get和set方法来赋值的,所以必须对上号 7 * 创建对象最重要的两点 对应的get和set方法 无参构造 8 */ 9 private int eid; //员工编号 10 private String ename; //员工姓名 11 private int did; //员工部门编号 12 public Employee() { 13 } 14 public int getEid() { 15 return eid; 16 } 17 public void setEid(int eid) { 18 this.eid = eid; 19 } 20 public String getEname() { 21 return ename; 22 } 23 public void setEname(String ename) { 24 this.ename = ename; 25 } 26 public int getDid() { 27 return did; 28 } 29 public void setDid(int did) { 30 this.did = did; 31 } 32 @Override 33 public String toString() { 34 return "Employee [eid=" + eid + ", ename=" + ename + ", did=" + did + "]"; 35 } 36 37 38 39 }
1 package com.jdbc.dbutils; 2 3 import java.sql.SQLException; 4 import java.util.List; 5 import java.util.Map; 6 7 import org.apache.commons.dbutils.QueryRunner; 8 import org.apache.commons.dbutils.handlers.BeanHandler; 9 import org.apache.commons.dbutils.handlers.BeanListHandler; 10 import org.apache.commons.dbutils.handlers.MapHandler; 11 import org.apache.commons.dbutils.handlers.MapListHandler; 12 import org.junit.Test; 13 14 import com.jdbc.entity.Employee; 15 import com.jdbc.usec3p0utils.C3p0Utils; 16 17 public class UseDbUtils { 18 @Test 19 public void dmlByDbutils() { 20 /* 21 * 使用c3p0连接池获取连接,但是有了Dbutils就省去了我们的连接和关闭工作 22 * org.apache.commons.dbutils.QueryRunner 该类常用方法 23 * public QueryRunner();无参构造,在之后调用qu 24 * public QueryRunner(DataSource ds);传一个数据源 c3p0工具包中连接池的数据源 25 * 26 */ 27 //第一种 有参构造 传数据源 28 QueryRunner qr = new QueryRunner(C3p0Utils.getDataSoruce()); 29 /* 30 * 增 31 */ 32 String sql = "insert into emp(eid,ename,did) values (?,?,?)"; 33 try { 34 // int update = qr.update(sql,4,"张飞",2);// 返回值是被影响的行数 35 // System.out.println(update); 36 // 或者 37 Object[] objs = {4,"张飞",2}; 38 int update = qr.update(sql,objs); 39 } catch (SQLException e) { 40 e.printStackTrace(); 41 } 42 43 //第二种 无参构造 44 QueryRunner qr2 = new QueryRunner(); 45 /* 46 * 删 47 */ 48 String sql2 = "delete from emp where did = ?"; //删除张飞这个员工 49 try { 50 int update2 = qr2.update(C3p0Utils.getConnection(),sql2, 3); 51 System.out.println(update2); 52 } catch (SQLException e) { 53 e.printStackTrace(); 54 } 55 56 } 57 /* 58 * 重点 将数据转化成对象 59 */ 60 /* //获取一个对象 使用public BeanHandler(Class type); 61 * public transient Object query(String sql, ResultSetHandler rsh, Object params[]); 62 * ResultSetHandler是BeanHandler它爸爸 Object params[]代表占位符?的具体值 63 */ 64 @Test 65 public void getEmpBeanByDbUtilsBeanHandler() throws SQLException { 66 67 QueryRunner qr = new QueryRunner(C3p0Utils.getDataSoruce()); 68 String sql = "select * from emp where eid = ?"; 69 Employee emp = qr.query(sql, new BeanHandler<Employee>(Employee.class),1); 70 System.out.println(emp); //执行结果 Employee [eid=1, ename=张三, did=1] 71 } 72 /* 73 * //获取一组对象 使用BeanListHandler 74 */ 75 @Test 76 public void getgetEmpsBeanByDbUtilsBeanListHandler() throws SQLException { 77 QueryRunner qr = new QueryRunner(C3p0Utils.getDataSoruce()); 78 String sql = "select * from emp"; 79 List<Employee> emps = qr.query(sql, new BeanListHandler<Employee>(Employee.class)); 80 System.out.println(emps); 81 //执行结果 [Employee [eid=1, ename=张三, did=1], Employee [eid=3, ename=小红, did=1], Employee [eid=4, ename=张飞, did=2]] 82 } 83 /* 84 * 获取对象,不用BeanHandler装对象 而用MapHandler输出对象值 85 * (注意,它输出的是一个对象,但是却不是一个Employee对象) 86 * 这是为连表查询做准备 87 * class MapHandler implements ResultSetHandler 88 * 键是字段 值是实数据 89 */ 90 @Test 91 public void getEmpValuesByDbUtilsMapHandler() throws SQLException { 92 93 QueryRunner qr = new QueryRunner(C3p0Utils.getDataSoruce()); 94 String sql = "select * from emp where eid = ?"; 95 Map<String, Object> empValues = qr.query(sql, new MapHandler(),1); 96 System.out.println(empValues); //并没有将值转换成一个员工对象 97 // 执行结果: {eid=1, ename=张三, did=1} 98 } 99 /* 100 * 不用BeanListHandler ,而是MapListHandler 101 */ 102 @Test 103 public void getEmpsValuesByDbUtilsMapListHandler() throws SQLException { 104 QueryRunner qr = new QueryRunner(C3p0Utils.getDataSoruce()); 105 String sql = "select * from emp"; 106 List<Map<String, Object>> empsValues = qr.query(sql, new MapListHandler()); 107 System.out.println(empsValues); //并没有将值转换成员工对象 108 //执行结果[{eid=1, ename=张三, did=1}, {eid=3, ename=小红, did=1}, {eid=4, ename=张飞, did=2}] 109 } 110 }
第四种组合:用于数据库中的连表
1.官方驱动 mysql-connector-java-5.1.13-bin.jar
2.C3p0连接池 和它的关联包 以及它的配置文件
c3p0-0.9.2-pre1.jar mchange-commons-0.2.jar c3p0-config.xml(配置文件放在src目录下)
3.commons-dbutils-1.4.jar Dbutils:主要是封装了JDBC的代码,简化dao层的操作。
4.自定义工具包 和它的关联包 chinasofti-tools.jar(调用了beanutils的方法) commons-beanutils-1.8.3.jar commons-logging-1.1.1.jar
包结构:
代码:
1 <?xml version="1.0" encoding="UTF-8" ?> 2 <c3p0-config> 3 <default-config> 4 <property name="jdbcUrl">jdbc:mysql://localhost:3306/db2</property> 5 <property name="driverClass">com.mysql.jdbc.Driver</property> 6 <property name="user">root</property> 7 <property name="password">123</property> 8 9 <property name="acquireIncrement">3</property> 10 <property name="initialPoolSize">2</property> 11 <property name="minPoolSize">2</property> 12 <property name="maxPoolSize">20</property> 13 </default-config> 14 </c
1 package com.jdbc.usec3p0utils; 2 3 import java.sql.Connection; 4 import java.sql.SQLException; 5 6 import com.mchange.v2.c3p0.ComboPooledDataSource; 7 /* 8 * 通过c3p0jar包封装一个工具类,c3p0中有一个连接池类,通过创建连接池来获取到一个dateSource对象,通过调用这个 9 * 对象的getConnection方法获取一个connection连接对象,这就是这个工具类的作用 10 * 11 * 对c3p0的理解 ,就是通过c3p0-config.xml配置文件,管理对数据库的连接,实现随时取用,提高效率 12 * 13 */ 14 public class C3p0Utils { 15 //C3P0连接池提供的核心类: 数据源(就是一个名词,作用是可以调用他的方法给我们一个连接对象) 16 private static ComboPooledDataSource dataSource=new ComboPooledDataSource(); 17 18 //获取数据源 19 public static ComboPooledDataSource getDataSoruce() { 20 return dataSource; 21 } 22 //从连接池中取用一个连接 23 public static Connection getConnection() throws SQLException { 24 return dataSource.getConnection(); 25 } 26 27 }
1 package com.jdbc.entity; 2 /* 3 * 由于有主外键,两个员工类封装的属性不一样,所以重新定义一个员工类 4 */ 5 public class EmployeeW { 6 private int eid; 7 private String ename; 8 /* 9 * 部门是主键 员工是外键 一个员工只有一个部门, 10 * 一个部门可以有多个员工 11 */ 12 private Department dep;//我们想得到部门名称而不是 部门id,所以定义了一个部门属性 13 public EmployeeW() { 14 } 15 public int getEid() { 16 return eid; 17 } 18 public void setEid(int eid) { 19 this.eid = eid; 20 } 21 public String getEname() { 22 return ename; 23 } 24 public void setEname(String ename) { 25 this.ename = ename; 26 } 27 public Department getDep() { 28 return dep; 29 } 30 public void setDep(Department dep) { 31 this.dep = dep; 32 } 33 @Override 34 public String toString() { 35 return "EmployeeW [eid=" + eid + ", ename=" + ename + ", dep=" + dep + "]"; 36 } 37 38 }
1 package com.jdbc.entity; 2 3 import java.util.List; 4 5 public class Department { 6 private int did; 7 private String dname; 8 private List<EmployeeW> emps; 9 public Department() { 10 } 11 public int getDid() { 12 return did; 13 } 14 public void setDid(int did) { 15 this.did = did; 16 } 17 public String getDname() { 18 return dname; 19 } 20 public void setDname(String dname) { 21 this.dname = dname; 22 } 23 public List<EmployeeW> getEmps() { 24 return emps; 25 } 26 public void setEmp(List<EmployeeW> emps) { 27 this.emps = emps; 28 } 29 @Override 30 public String toString() { 31 return "Department [did=" + did + ", dname=" + dname + ", emps=" + emps + "]"; 32 } 33 34 35 }
1 package com.jdbc.dbutils; 2 3 import java.sql.SQLException; 4 import java.util.List; 5 import java.util.Map; 6 7 import org.apache.commons.dbutils.QueryRunner; 8 import org.apache.commons.dbutils.handlers.BeanListHandler; 9 import org.apache.commons.dbutils.handlers.MapHandler; 10 import org.junit.Test; 11 12 import com.chinasofti.commons.CommonUtils; 13 import com.chinasofti.jdbc.TxQueryRunner; 14 import com.jdbc.entity.Department; 15 import com.jdbc.entity.EmployeeW; 16 17 18 19 public class UseDbUtilsAndChinaSoft { 20 /* 21 * 第一种 :query(sql, new MapHandler(),1); 22 * 第二种 :qr.query(sql, new BeanHandler<EmployeeW>(EmployeeW.class),1); 23 */ 24 @Test 25 public void getEmp() throws SQLException{ 26 //使用封装好的TxQueryRunner省去获取数据源的步骤 27 QueryRunner qr = new TxQueryRunner(); 28 //连表 29 String sql = "select * from emp e,dep d where e.did = d.did and e.eid =? "; 30 Map<String, Object> map = qr.query(sql, new MapHandler(),1); 31 //// 或者直接获取员工对象和部门对象org.apache.commons.dbutils.handlers.BeanHandler 32 // EmployeeW emp = qr.query(sql, new BeanHandler<EmployeeW>(EmployeeW.class),1); 33 // Department dep = qr.query(sql, new BeanHandler<Department>(Department.class),1); 34 // emp.setDep(dep); 35 // System.out.println(emp); 36 //// 执行结果 37 //// EmployeeW [eid=1, ename=张三, dep=Department [did=1, dname=财务部, emps=null]] 38 /* 39 * 自定义的ChinaSoft jar包中封装一个将map转化成给定对象的方法 40 * toBean方法内部调用了beanUtils的BeanUtils.populate(bean, map); 41 */ 42 EmployeeW empbean = CommonUtils.toBean(map,EmployeeW.class);//得到一个员工对象 43 Department depbean = CommonUtils.toBean(map, Department.class);//得到一个部门对象 44 // 把部门对象作为属性赋给员工 45 empbean.setDep(depbean); 46 System.out.println(empbean);//得到员工的信息 47 //输出结果 48 // EmployeeW [eid=1, ename=张三, dep=Department [did=1, dname=财务部, emps=null]] 49 // 50 } 51 /* 52 * 获取部门信息,部门中有多少人 53 */ 54 @Test 55 public void getDep() throws SQLException{ 56 //使用封装好的TxQueryRunner省去获取数据源的步骤 57 QueryRunner qr = new TxQueryRunner(); 58 String sql = "select * from dep where did =? "; 59 //得到一个部门对象 用map或者bean 60 Map<String, Object> map = qr.query(sql, new MapHandler(),1); 61 Department depbean = CommonUtils.toBean(map, Department.class); 62 String sql2 ="select * from emp where did =?"; 63 //得到所有员工集合 直接转成list集合,如果用maplist还要遍历list集合再把每个map转化成员工对象太麻烦 64 List<EmployeeW> emps = qr.query(sql2, new BeanListHandler<EmployeeW>(EmployeeW.class),1); 65 depbean.setEmp(emps); 66 System.out.println(depbean); 67 // 执行结果 68 // Department [did=1, dname=财务部, emps=[EmployeeW [eid=1, ename=张三, dep=null], EmployeeW [eid=3, ename=小红, dep=null]]] 69 70 71 72 73 74 75 } 76 77 }