0%

MySQL的基本操作可以包括两个方面:MySQL常用语句如高频率使用的增删改查(CRUD)语句和MySQL高级功能,如存储过程,触发器,事务处理等。而这两个方面又可以细分如下:

MySQL基本操作总结.png

  • MySQL常用语句
    1. 表(或者数据库)的CRUD
    2. 表数据的CRUD,其中表数据查询使用最多,也更复杂。查询可以按照单表还是多表可以分为:单表SELECT查询和多表的联结查询(INNER JOIN, LEFT JOIN, RIGHT JOIN和FULL JOIN)以及组合查询UNION和UNION ALL
    3. SQL语句中各个关键字的执行顺序
  • MySQL的高级功能
    1. 存储过程
    2. 事务处理
    3. 触发器

1. 表(或数据库)操作语句

1.1. 查询表(或数据库)

  1. 获取所有可用的数据库SHOW DATABASES
  2. 选择数据库USE customers
  3. 用于显示数据库服务器的状态信息:SHOW STATUS
  4. 用来显示授权用户的安全权限:SHOW GRANTS
  5. 用来显示数据库服务器或警告信息:SHOW ERRORS 或者 SHOW WARNINGS
  6. 用于显示创建数据库时的创建语句SHOW CREATE DATABASE customers
  7. 用于显示创建表时的创建语句SHOW CREATE TABLE customers
  8. 获取当前所选的数据库中所有可用的表SHOW TABLES
  9. 获取表中所有列的信息SHOW COLUMNS FROM tableName;同时DESCRIBE语句有相同的效果:DESCRIBE tableName

1.2. 新建表(或)数据库

  1. 新建数据库CREATE DATABASE customers;

  2. 创建表可以使用CREATE TABLE语句:

    1
    2
    3
    4
    5
    6
    CREATE TABLE customers(
    cust_id INT NOT NULL AUTO_INCREMENT,
    cust_name CHAR(50) NOT NULL,
    cust_age INT NULL DEFAULT 18,
    PRIMARY KEY(cust_id)
    )ENGINE=INNODB;

    有这样一些细节:

    1. 允许NULL值,则说明在插入行数据时允许不给出该列的值,而NOT NULL则表示在插入或者更新该列数据,必须明确给出该列的值;
    2. DEFAULT表示该列的默认值,在插入行数据时,若没有给出该列的值就会使用其指定的默认值;
    3. PRIMARY KEY用于指定主键,主键可以指定一列数据,而可以由多列数据组合构成,如PRIMARY KEY(cust_id,cust_name)
    4. ENGINE用于指定引擎类型。常见的引擎类型有这些:(1)InnoDB是一个支持可靠的事务处理的引擎,但是不支持全文本搜索;(2)MyISAM是一个性能极高的引擎,它支持全文本搜索,但是不支持事务处理;(3)MEMORY在功能上等同于MyISAM,但由于数据存储在内存中,速度很快(特别适合于临时表);
  3. 在创建表的时候可以使用FOREIGN KEY来创建外键,即一个表中的FOREIGN KEY指向另一个表中PRIMARY KEY。外键FOREIGN KEY用于约束破坏表的联结动作,保证两个表的数据完整性。同时也能防止非法数据插入外键列,因为该列值必须指向另一个表的主键。实例为:

    1
    2
    3
    4
    5
    6
    7
    8
    CREATE TABLE Orders
    (
    Id_O int NOT NULL,
    OrderNo int NOT NULL,
    Id_P int,
    PRIMARY KEY (Id_O),
    FOREIGN KEY (Id_P) REFERENCES Persons(Id_P)
    )

1.3 删除表(或数据库)

  1. 删除数据库DROP DATABASE customers
  2. 删除表,使用DROP TABLE子句:DROP TABLE customers

1.4 更新表

  1. 更新表结构信息可以使用ALTER TABLE子句,如为表增加一列:ALTER TABLE vendors ADD vend_name CHAR(20);另外经常用于定义外键,如:

    1
    2
    3
    ALTER TABLE customers 
    ADD CONSTRAINT fk_custormer_orders
    FOREIGN KEY(cust_id) REFERENCES orders (order_cust)
  2. 重命名表,使用RENAME子句RENAME TABLE backup_customers TO customers, backup_vendors TO vendors;更改多个表名,之间用逗号间隔

2 表数据操作语句

2.1 查询表数据

基本查询语句

  1. 根据过滤条件查询表中的单列或者多列或者全部列的信息SELECT FROM WEHERESELECT cust_id,cust_name FROM customers WHERE cust_id=10086;其中过滤条件操作符有:=,<>,!=,<,<=,>,>=,BETWEEN AND,IS NULL;
  2. 为查询出的某一列信息去重DISTINCTSELECT DISTINCT cust_name FROM customers
  3. 限制单列查询结果的行数:SELECT cust_name FROM customers LIMIT 5;LIMIT后跟一个数值,表示从第0行开始取,共取5行数据;如果LIMIT 5,5表示从第5行(数据库中实际第6行记录)开始取,共取5行数据。注意:数据是从第0行开始计数的;
  4. ORDER BY子句取一个或者多个列,据此对输出进行排序:SELECT cust_id,cust_name FROM customers ORDER BY cust_id DESC, cust_name;
  5. IN操作符用来指定条件范围,范围中的每个条件都可以进行匹配:SELECT cust_id, cust_name FROM customers WHERE cust_id IN (1000,2000)。另外,NOT操作符可以和IN操作符配合使用,用于表示检索出不符合条件的所有数据;
  6. LIKE操作符用来表明模糊查询,与之配合使用的通配符有%,%表示任何字符出现任何次数;__表示只能匹配一个字符:SELECT cust_id,cust_name FROM customers WHERE cust_name LIKE '%happy%'
  7. 使用分组查询并可以满足一定的分组过滤条件GROUP BY HAVING。如检索总计订单金额大于等于50的订单号和订单总金额,并按总金额进行排序:SELECT order_num,SUM(quantity*item_price) AS order_total FROM orderitems GROUP BY order_num HAVING SUM(quantity*item_price)>=50 ORDER BY order_total
  8. WHERE和HAVING的比较。WHERE是行级过滤,而HAVING是组级过滤。被WHERE过滤掉的数据不会出现在分组中。WHERE中通配符以及多个WHERE子句的连接同样适用于HAVING子句;
  9. GROUP BY的使用注意事项: (1)GROUP BY子句中可以嵌套分组(即通过多个列进行分组GROUP BY cust_id, cust_name),但是进行数据汇总时,是在最后规定的分组上进行;(2)GROUP BY子句中列出的每个列都必须是检索列或者是有效的表达式。(3)如果有NULL值,将值NULL作为一个分组进行返回,如果有多行NULL值,它们将分为一组
  10. 嵌套其他查询中的查询,称之为子查询。执行过程由里向外,里层查询结果作为外层查询的条件:SELECT cust_id FROM orders WHERE order_num IN (SELECT order_num FROM orderitems WHERE prod_id = 'happy')。当然,多表的查询可以是用联结查询。

联结查询

  1. 内联结用又称之为内部联结,是基于两个表 之间的的相等测试。如果不加过滤条件,会造成“笛卡尔积”。SELECT vend_name,prod_name,prod_price FROM vendors INNER JOIN products ON vendors.vend_id=products.vend_id;同样可以使用WHERE进行多表联结查询,但是更推荐使用INNER JOIN等联结方式;
  2. 外部联结包括左外联结LEFT JOIN和右外联结RIGHT JOIN和全连接FULL JOIN。例如查询每个客户的订单数:SELECT customers.cust_id,orders.orders_num FROM customers LEFT JOIN orders ON orders.cust_id =customers.cust_id;LEFT JOIN 会全部返回左表数据,RIGHT JOIN会全部返回右表数据,FULL JOIN会将左右两个表的数据全部返回;
  3. 联结查询与聚集函数一起使用。如查询每个客户的订单数:SELECT customers.cust_name,customers.cust_id,COUNT(orders.order_num) AS num_ord FROM customers INNER JOIN orders ON customers.cust_id=orders.cust_id GROUP BY customers.cust_id

组合查询

  1. 多个查询(SELECT)可以使用UNION将多个查询结果进行合并成一个结果集返回,UNION必须包含两个及两个以上的SELECT查询,并且每个传必须包含相同的列、表达式或聚集函数,数据类型不必完全相同,MySQL会进行隐式的类型转换。SELECT vend_id,prod_id,prod_price FROM products WHERE prod_price>5 UINON SELECT vend_id,prod_id,prod_price FROM products WHERE vend_id IN (1001,1002);
  2. UNION返回的是去重后的结果,如果不需要去重则可以使用UNION ALL
  3. 可以多组合查询使用ORDER BY进行排序,但是是针对的最终的结果集进行排序,而不是其中单个SELECT查询进行排序,因此对于组合查询来说ORDER BY子句只有一个。SELECT vend_id,prod_id,prod_price FROM products WHERE prod_price>5 UINON SELECT vend_id,prod_id,prod_price FROM products WHERE vend_id IN (1001,1002) ORDER BY vend_id

使用函数对数据进行处理

  1. 拼接列名:SELECT Concat (vendName,'(',vendCountry,')') FROM vendors ORDER BY vendName
  2. 执行算术表达式计算:SELECT prodId, quantity,price, quantity*price AS expandedPrice FROM orderItems
  3. 文本处理函数如Upper(),LTrim(),RTrim()等函数。比如使用Upper函数将文本转换成大写:SELECT vendName, Upper(vendName) FROM vendors ORDER BY vendName
  4. 时间和日期处理函数,如Date(),Day()等。SELECT custId, orderNum FROM orders WHERE Date(orderDate)='2015-09-01'
  5. 数值处理函数,如Abs(),Cos()等;
  6. 常用的聚集函数。如AVG(),COUNT(),MAX(),MIN()以及SUM()。SELECT COUNT(*) AS numbers, MIN(prod_price) AS price_min, MAX(prod_price) AS price_max,AVG(prod_price) AS price_avg FROM products

2.2 插入表数据

  1. 向表中插入行数据可以使用INSERT INTO子句,更安全的方式是指定列名。INSERT INTO customers (cust_name, cust_email) VALUES('happy','happy@gmail.com');在INSERT INTO子句中能够省略列名的前提条件是:该列可以允许定义为NULL值或者在定义该列时给出去了默认值;
  2. 如果插入多行数据可以将多组值用逗号进行分隔即可。INSERT INTO customers (cust_name, cust_email) VALUES('happy','happy@gmail.com'),('smart','smart@gmail.com')
  3. 将查询出来的数据插入表中,可以使用INSERT SELECT语句。INSERT INTO customers(cust_id,cust_contact) SELECT cust_id, cust_contact FROM customers WHERE cust_id>5;其中SELECT中可以带WHERE过滤条件;INSERT SELECT通常被用于复制表数据

2.3 更新表数据

  1. 如果要更新表数据的话,使用UPDATE子句:UPDATE customers SET cust_name ='happy',cust_email='happy@gmail.com' WHERE cust_id = 1001
  2. 注意:如果不加WHERE条件指定到某一行的话,会更新表中某一列全部的数据

2.4 删除表数据

  1. 如果从表中删除数据的话,可以使用DELETE子句。DELETE FROM customers WHERE cust_id = 10086;删除的数据必定是表中行数据,而不是某一列。因此,与UPDATE子句相比,DELETE子句并不需要指定是哪一列,而仅仅只需要指定具体的表名即可;
  2. 注意:如果不添加WHERE指定条件的话,会将整个表中所有行数据全部删除。另外,DELETE只是删除表中的数据,而不会删除表结构信息;
  3. 如果想删除表中全部的数据,可以使用TRUNCATE,比DELETE删除效率更高;

3. SQL中关键字执行顺序

在SQL语句中每个关键字都会按照顺序往下执行,而每一步操作,会生成一个虚拟表,最后产生的虚拟表会作为执行的最终结果返回。下面的是常用的关键字的执行顺序:

1
2
3
4
5
6
7
8
9
10
(8)SELECT (9)DISTINCT<select_list>
(1)FROM <left_table>
(3)<join_type> JOIN <right_table>
(2) ON <join_condition>
(4)WHERE <where_condition>
(5)GROUP BY<group_by_list>
(6)WITH{CUBE|ROLLUP}
(7)HAVING<having_condition>
(10)ORDER BY<order_by_list>
(11)LIMIT<limit_number>
  1. FROM:对FROM左边的表和右边的表计算笛卡尔积,产生虚表VT1;
  2. ON:对虚拟表VT1进行ON筛选,只有那些符合条件的行才会被记录在虚拟表VT2中;
  3. JOIN:如果是OUT JOIN,那么将保留表中(如左表或者右表)未匹配的行作为外部行添加到虚拟表VT2中,从而产生虚拟表VT3;
  4. WHERE:对虚拟表VT3进行WHERE条件过滤,只有符合的记录才会被放入到虚拟表VT4;
  5. GROUP BY:根据GROUP BY子句中的列,对虚拟表VT4进行分组操作,产生虚拟表VT5;
  6. CUBE|ROLLUP:对虚拟表VT5进行CUBE或者ROLLUP操作,产生虚拟表VT6;
  7. HAVING:对虚拟表VT6进行HAVING条件过滤,只有符合的记录才会被插入到虚拟表VT7中;
  8. SELECT:执行SELECT操作,选择指定的列,插入到虚拟表VT8中;
  9. DISTINCT:对虚拟表VT8中的记录进行去重,产生虚拟表VT9;
  10. ORDER BY:将虚拟表VT9中的记录按照进行排序操作,产生虚拟表VT10;
  11. LIMIT:取出指定行的记录,产生虚拟表VT11,并将结果返回。

4. 索引

MySQL索引的建立对于MySQL的高效运行是很重要的,索引可以大大提高MySQL的检索速度。索引分单列索引和组合索引。单列索引,即一个索引只包含单个列,而组合索引,即一个索引包含多个列。

4.1 创建索引

创建索引有两种方式,一种是直接利用CREATE INDEX进行创建,另外一种则是通过修改表结构来进行添加,则是利用ALTER TABLE语句。

  1. 使用CREATE INDEX

    语法为:

    1
    2
    3
    CREATE [UNIQUE|FULLTEXT|SPATIAL] INDEX index_name
    [USING index_type]
    ON table_name (index_col_name,...)

    其中对应的语法变量信息如下:

    [UNIQUE|FULLTEXT|SPATIAL]

    其中括号中的这三个关键字表示创建的索引类型,它们分别表示唯一索引全文索引空间索引三种不同的索引类型。如果我们不指定任何关键字,则默认为普通索引。

    index_name

    index_name表示索引的名称,由用户自行定义,以便于以后对该索引进行修改等管理操作。

    index_type

    index_type表示索引的具体实现方式,在MySQL中,有两种不同形式的索引——BTREE索引和HASH索引。在存储引擎为MyISAM和InnoDB的表中只能使用BTREE,其默认值就是BTREE;在存储引擎为MEMORY或者HEAP的表中可以使用HASH和BTREE两种类型的索引,其默认值为HASH。

    index_colname

    index_col_name表示需要创建索引的字段名称,我们还可以针对多个字段创建复合索引,只需要在多个字段名称之间以英文逗号隔开即可。此外,对于CHAR或VARCHAR类型的字段,我们还可以只使用字段内容前面的一部分来创建索引,只需要在对应的字段名称后面加上形如(length)的指令即可,表示只需要使用字段内容前面的length个字符来创建索引。在这里,我们以customers表的cust_name字段(类型为VARCHAR(50))为例,使用cust_name字段的6个字符前缀来创建索引。

    1
    CREATE INDEX idx_cust_name ON user (cust_name(6));
  2. 使用ALTER TABLE

    语法为:

    1
    2
    3
    ALTER TABLE table_name
    ADD [UNIQUE|FULLTEXT|SPATIAL] INDEX index_name
    (index_col_name,...) [USING index_type]

4.2 删除索引

删除指定表中指定名称的索引,语法为:

1
2
ALTER TABLE table_name
DROP INDEX index_name;

例如删除名称为idx_cust_name的索引,其SQL语句为:

1
2
ALTER TABLE customers
DROP INDEX idx_cust_name;

4.3 修改索引

在MySQL中并没有提供修改索引的直接指令,一般情况下,我们需要先删除掉原索引,再根据需要创建一个同名的索引,从而变相地实现修改索引操作。

1
2
3
4
5
--先删除
ALTER TABLE user
DROP INDEX idx_user_username;
--再以修改后的内容创建同名索引
CREATE INDEX idx_cust_name ON customers (cust_name(8));

4.4 查看索引

在MySQL中,要查看某个数据库表中的索引也非常简单,只需要使用以下两个命令中的任意一种即可。

1
2
3
4
--如果查看索引前,没有使用user db_name等命令指定具体的数据库,则必须加上FROM db_name
SHOW INDEX FROM table_name [FROM db_name]
--如果查看索引前,没有使用user db_name等命令指定具体的数据库,则必须加上db_name.前缀
SHOW INDEX FROM [db_name.]table_name

5. 存储过程

  1. 什么是存储过程?存储过程简单来说,就是为了复用性或者实现复杂的业务功能,而保存的一条或多条MySQL语句的集合,可将其视为批文件;
  2. 为什么使用存储过程?(1)通过把处理封装在容易使用的单元中,简化复杂的操作;(2)由于不要求反复建立一系列处理步骤,这保证了数据的完整性,如果所有的开发人员和应用程序都使用同一存储过程,则所使用的代码都是相同的;(3)简化对变动的管理。如果表名、列名或业务逻辑有变化,只需要更改存储过程的代码,使用它的开发人员甚至不需要知道这些变化,也就是具备了安全性;(4)提高了性能,因为使用存储过程比单独使用SQL语句要快;(5)存储过程可用来编写功能更灵活的代码。因此,存储过程的具备三个特性:简单可复用、安全以及高性能
  3. 存储过程的缺点?(1)存储过程编写比基本的SQL语句更加复杂,需要更高的技能;(2)可能没有创建存储过程的权限,数据库管理员可能会限制创建存储过程的权限,允许用户使用存储过程,而不允许用户自由创建存储过程;

创建存储过程

  1. 创建存储过程。如需要统计用户订单总金额,如果该用户需要交税的话,订单总金额则需要再加上税费

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    DELIMITER //
    CREATE PROCEDURE ordertotal(
    IN custid INT,
    IN taxable BOOLEAN,
    OUT ototal DECIMAL(8,2)
    )COMMENT 'obtain total order price'

    BEGIN

    /*declare variable for total*/
    DECLARE total DECIMAL(8,2);
    DECLARE taxrate INT DEFAULT 6;


    /*get the order total*/

    SELECT SUM(item_price*item_quantity) INTO total
    FROM customers
    WHERE cust_id = custid;

    /*is this taxable?*/

    IF taxable THEN
    SELECT total+(total/100*taxrate) INTO total;
    END IF;
    SELECT total INTO ototal;
    END //

有这样一些细节: 1. 使用CREATE PROCEDURE语句进行创建,()圆括号中为存储过程的参数,其中参数类型有:1. IN类型,表示传递给存储过程;2. OUT 类型,表示存储过程返回的结果,在调用存储过程时需要传入@开始的变量;3. INOUT类型,表示在存储过程中可以传入和传出; 2. DECLARE用来声明一个变量,如这里的total,taxrate。注意MySQL中定义变量时都是变量名在前,数据类型在后。 3. 存储过程具体逻辑写在BEGIN END之间; 4. 将值赋给变量使用INTO关键字; 5. 由于存储过程中每个SQL语句中用;作为分隔符,会和单个SQL造成冲突,因此可使用DELIMITER重新定义分类符,如该例子中定义//为分隔符,自然存储过程结尾就用END //结尾,而不再是END。同时,分隔符//成对出现后,恢复到默认的”;”作为分隔符;

执行存储过程

  1. 使用CALL子句执行存储过程,CALL子句接受存储过程的名称以及需要传递的参数。

    1
    2
    CALL ordertotal(1,TRUE,@total);
    SELECT @total;

如果存储过程中定义了OUT类型的输入参数,那么在执行存储过程时需要传入变量,如这里@total,并且变量都是用@开始的。如果存储过程中没有参数的话,就用空圆括号表示即可,CALL ordertotal()

删除存储过程

  1. 删除存储过程,可以使用DROP PROCEDURE子句。如DROP PROCEDURE ordertotal

查询存储过程

  1. 显示创建一个存储过程的语句,可以使用SHOW CREATE PROCEDURE。如SHOW CREATE PROCEDURE ordertotal
  2. 查询所有存储过程的状态,如果在定义存储过程中使用COMMENT添加注释,可以查看。同时可以LIKE进行过滤结果。如SHOW PROCEDURE STATUS LIKE '%order%';

6. 事务处理

  1. 什么是事务?

    事务处理是用来维护数据库的完整性,它保证成批的MySQL操作要么完全执行,要么完全不执行。事务处理是一种机制,用来管理必须成批执行的MySQL操作,它们要么时作为整体执行或者完全不执行。

  2. 关键概念:

    1. 事务:是指一组SQL语句;
    2. 回退:是指撤销指定的SQL语句的过程;
    3. 提交:指将未存储的SQL语句的结果写入数据库表中;
    4. 保留点:指事务处理中设置的临时占位符,可以对它发布回退;
  3. 如何创建执行事务?

    1
    2
    3
    4
    5
    6
    START TRANSACTION;
    INSERT INTO customers (cust_name,item_price,item_quantity) VALUES ('1',5,18);
    SELECT * FROM customers;
    SAVEPOINT insertinto;
    INSERT INTO customers (cust_name,item_price,item_quantity) VALUES ('2',5,18);
    ROLLBACK TO insertinto;

    执行结果为:插入数据(‘1’,5,18)有效,因为,只会从保留点SAFEPOINT之后开始回退,也就是说保留点SAFEPOINT之前的SQL语句执行的结果仍然有效。

    有这样一些细节:

    1. STAET TRANSACTION用来表示下面的SQL语句集为一段事务;
    2. SAFEPOINT 用于指定保留点insertinto;
    3. ROLLBACK TO表示从指定保留点开始回退,也就是说保留点之前的SQL语句执行结果依然有效。如果仅仅使用ROLLBACK进行回退的话就表示从STAET TRANSACTION之后所有的SQL语句执行效果都会撤销;
  4. MySQL提交(写或保存)操作是自动进行的,这称之为隐含提交。但是在事务处理块中,提交不会隐含进行,要使用COMMIT子句进行提交。如:

    1
    2
    3
    4
    START TRANSACTION;
    INSERT INTO customers (cust_name,item_price,item_quantity) VALUES ('1',5,18);
    INSERT INTO customers (cust_name,item_price,item_quantity) VALUES ('2',5,18);
    COMMIT;

采用COMMIT提交事务,如果两条SQL语句都执行成功,才会将数据都写入表中。

7. 触发器

  1. 什么是触发器?

    当某条SQL语句发生时,自动执行某些其他的SQL语句的时候就需要使用到触发器。触发器只能响应:DELETE,INSERT,UPDATE这三个特定操作。

  2. 创建触发器?

    创建触发器时需要给出最重要的四条信息:1.全局唯一的触发器名;2.触发器关联的表;3.触发器在何时执行(操作执行之前或者之后)4.触发器应该响应的活动(DELETE, INSERT或者UPDATE);

    由于触发器只能响应特定的三种类型的操作,因此可创建的触发器也就三种类型:INSERT触发器,DELETE触发器以及UPDATE触发器。

    INSERT触发器

    在执行INSERT触发器时,也这样几点需要注意:1.在INSERT触发器代码内,可以引用一个名为NEW的虚拟表,可以用NEW来访问刚插入的行数据;2.在BEFORE INSERT触发器中,NEW中的值可以被更新;3.对于AUTO_INCREMENT列,NEW在INSERT执行之前包含0,在INSERT执行之后包含新的自定生成值。

    创建一个INSERT触发器,每次插入一行数据,每次会返回当前插入的行数据的id。

    1
    2
    3
    4
    5
    6
    7
    /*创建触发器*/
    CREATE TRIGGER insertcustomers AFTER INSERT ON customers
    FOR EACH ROW SELECT NEW.cust_id INTO @newinsertid;

    /*执行触发器*/
    INSERT INTO customers (cust_name,item_price,item_quantity) VALUES ('2',5,18);
    SELECT @newinsertid;

    有这样一些细节:

    1. 使用CREATE TRIGGER来创建触发器;
    2. AFTER INSERT表明在插入行数据之后,触发器才会执行特征操作;
    3. FOR EACH ROW 表示对插入的每一行数据,触发器都起作用;
    4. 针对INSERT触发器,可以使用虚拟表NEW,来使用刚插入的行数据。比如例子中,SELECT NEW.cust_id INTO @newinsertid表示将新插入的行数据的id赋值给变量@newinsertid;

    DELETE触发器

    DELETE触发器在DELETE语句执行之前或者之后,需要知道以下两点:

    1. 在DELETE触发器代码内,可以引用一个名为OLD的虚拟表,来访问被删除的行;
    2. OLD表中的数据只能读,不能被更新,而在INSERT触发器中,就可以通过NEW来更新被插入的行数据;

    例如,针对customers表,当删除一行数据时,返回被删除数据的cust_id以及cust_name:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    /*创建DELETE触发器*/

    DELIMITER //
    CREATE TRIGGER insertcustomers AFTER DELETE ON customers
    FOR EACH ROW
    BEGIN
    SELECT OLD.cust_name INTO @deletecustname;
    SELECT OLD.cust_id INTO @deletecustid;
    END //

    /*调用DELETE触发器*/
    DELETE FROM customers WHERE cust_id = 3;
    SELECT @deletecustname;
    SELECT @deletecustid;

    基本上与创建INSERT触发器一样,只不过在DELETE触发器中只能使用OLD来访问被删除的行数据。

    UPDATE触发器

    UPDATE触发器在UPDATE语句执行之前或者之后执行,需要知道一下几点:

    1. 在BEFORE UPDATE触发器中可以使用NEW和OLD来访问数据,而在AFTER UPDATE触发器中使用NEW来访问数据会报错,只能使用OLD来访问数据;
    2. 在BEFORE UPDATE触发器中,NEW中的值可以被改变,即允许更改将用于UPDATE的数据;
    3. OLD中的行数据只能读,不能被更新;

    一个UPDATE触发器示例如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /*创建UPDATE触发器*/

    DELIMITER //
    CREATE TRIGGER insertcustomers BEFORE UPDATE ON customers
    FOR EACH ROW
    BEGIN

    SELECT NEW.cust_name INTO @beforeupdate;
    SET NEW.cust_name = 'reset_name';
    SELECT OLD.cust_name INTO @afterupdate;
    END //

    /*调用UPDATE触发器*/
    UPDATE customers SET cust_name = 'happy' WHERE cust_id = 5;
    SELECT @beforeupdate;
    SELECT @afterupdate;

    输出为@beforeupdate为‘happay’,而@afterupdate为’reset_name’。有这样一些细节:

    1. NEW虚拟表中的数据可以更改,如这里采用 SET NEW.cust_name = 'reset_name';,将待更新的cust_name由“happy”变成了“reset_name”
    2. 在BEFORE UPDATE触发器中可以使用NEW和OLD来访问数据,而在AFTER UPDATE触发器中使用NEW来访问数据会报错;
  3. 删除触发器?

    删除触发器,可以使用 DROP TRIGGER语句,比如DROP TRIGGER insertcustomers;。触发器不能更新或者覆盖,如果要修改触发器,必须删除这个触发器。


本文整理自

MySQL命令,一篇文章替你全部搞定

仅做个人学习总结所用,遵循CC 4.0 BY-SA版权协议,如有侵权请联系删除!


Lock接口

锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源(但是有些锁可以允许多个线程并发的访问共享资源,比如读写锁)。

在Lock接口出现之前,Java程序是靠synchronized关键字实现锁功能的,而Java 5之后,并发包中新增了Lock接口(以及相关实现类)用来实现锁功能,它提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁。虽然它缺少了(通过synchronized块或者方法所提供的)隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class X{
//定义锁对象
private final ReentrantLock lock=new ReentrantLock();
//定义需要保证线程安全的方法
public void m(){
//加锁
lock.lock();
try{
//...method body
}
//使用finally块来保证释放锁
finally{
lock.unlock();
}
}
}

使用Reentrantlock可以进行尝试锁定tryLock(),这样无法锁定,或者在指定时间内无法锁定,返回false;

使用ReentrantLock还可以调用lockInterruptibly()方法,可以对线程interrupt()方法做出响应,在一个线程等待锁的过程中,可以被打断,打断后会抛异常。

自己实现一个锁

自旋实现锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class SpinLock {
//原子引用线程
AtomicReference<Thread> atomicReference = new AtomicReference<>();

public void mylock() {
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "\t come in");
// 自旋获取锁
while (!atomicReference.compareAndSet(null, thread)) {

}
}

public void myUnlock() {
Thread thread = Thread.currentThread();
// CAS解锁
atomicReference.compareAndSet(thread, null);
System.out.println(Thread.currentThread().getName()+"\t invoked myunlock()");
}
}

缺点:耗费CPU资源,没有竞争到锁的线程会一直占用CPU资源进行CAS操作。

park+自旋实现锁

Java提供了一个较为底层的并发工具类:LockSupport,可以让线程停止下来(阻塞),还可以唤醒线程。

1
2
3
4
// 阻塞线程
LockSupport.park(Object blocker)
// 唤醒线程
LockSupport.unpark(Thread thread)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class SpinLock {
// 原子引用线程
AtomicReference<Thread> atomicReference = new AtomicReference<>();
// 阻塞线程队列
Queue<Thread> parkQueue = new LinkedBlockingQueue<>();

public void mylock() {
System.out.println(Thread.currentThread().getName() + "\t come in");
// 自旋获取锁
while (!atomicReference.compareAndSet(null, thread)) {
park();
}
}

public void myUnlock() {
Thread thread = Thread.currentThread();
// CAS解锁
atomicReference.compareAndSet(thread, null);
System.out.println(Thread.currentThread().getName()+"\t invoked myunlock()");
lock_notify();
}

public void park() {
parkQueue.add(Thread.currentThread());
LockSupport.park(Thread.currentThread());
}

public void unpark() {
Thread t = parkQueue.poll();
LockSupport.unpark(t);
}
}

队列同步器AQS

队列同步器AbstractQueuedSynchronizer(AQS)是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作,并发包的作者(Doug Lea)期望它能够成为实现大部分同步需求的基础。

AQS的实现

FIFO队列

同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。

AQS中的节点Node:

1
2
3
4
5
6
7
8
9
10
static final class Node {
// 等待状态,若值为-1,表示后继节点处于等待状态
volatile int waitStatus;
// 前一个节点
volatile Node prev;
// 下一个节点
volatile Node next;
// 节点绑定线程
volatile Thread thread;
}

AQS的属性:

1
2
3
4
5
6
7
8
public abstract class AbstractQueuedSynchronizer {
// 等待队列头结点
private transient volatile Node head;
// 等待队列尾结点
private transient volatile Node tail;
// 状态
private volatile int state;
}

​ 未完待续


本文整理自

Java中的锁及AQS实现原理

仅做个人学习总结所用,遵循CC 4.0 BY-SA版权协议,如有侵权请联系删除!


自己实现一个锁

自旋实现锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class SpinLock {
//原子引用线程
AtomicReference<Thread> atomicReference = new AtomicReference<>();

public void mylock() {
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "\t come in");
// 自旋获取锁
while (!atomicReference.compareAndSet(null, thread)) {

}
}

public void myUnlock() {
Thread thread = Thread.currentThread();
// CAS解锁
atomicReference.compareAndSet(thread, null);
System.out.println(Thread.currentThread().getName()+"\t invoked myunlock()");
}
}

缺点:耗费CPU资源,没有竞争到锁的线程会一直占用CPU资源进行CAS操作。

park+自旋实现锁

Java提供了一个较为底层的并发工具类:LockSupport,可以让线程停止下来(阻塞),还可以唤醒线程。

1
2
3
4
// 阻塞线程
LockSupport.park(Object blocker)
// 唤醒线程
LockSupport.unpark(Thread thread)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class SpinLock {
// 原子引用线程
AtomicReference<Thread> atomicReference = new AtomicReference<>();
// 阻塞线程队列
Queue<Thread> parkQueue = new LinkedBlockingQueue<>();

public void mylock() {
System.out.println(Thread.currentThread().getName() + "\t come in");
// 自旋获取锁
while (!atomicReference.compareAndSet(null, thread)) {
park();
}
}

public void myUnlock() {
Thread thread = Thread.currentThread();
// CAS解锁
atomicReference.compareAndSet(thread, null);
System.out.println(Thread.currentThread().getName()+"\t invoked myunlock()");
lock_notify();
}

public void park() {
parkQueue.add(Thread.currentThread());
LockSupport.park(Thread.currentThread());
}

public void unpark() {
Thread t = parkQueue.poll();
LockSupport.unpark(t);
}
}

concurrent包的结构层次

在针对并发编程中,Doug Lea大师为我们提供了大量实用,高性能的工具类,针对这些代码进行研究会让我们队并发编程的掌握更加透彻也会大大提升我们队并发编程技术的热爱。这些代码在java.util.concurrent包下。如下图,即为concurrent包的目录结构图。

concurrent目录结构.pngconcurrent目录结构.png

其中包含了两个子包:atomic以及lock,另外在concurrent下的阻塞队列以及executors,这些就是concurrent包中的精华,之后会一一进行学习。而这些类的实现主要是依赖于volatile以及CAS(关于volatile可以看这篇文章,关于CAS可以看这篇文章的3.1节),从整体上来看concurrent包的整体实现图如下图所示:

concurrent包实现整体示意图.pngconcurrent包实现整体示意图.png

lock简介

我们下来看concurent包下的lock子包。锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源。在Lock接口出现之前,java程序主要是靠synchronized关键字实现锁功能的,而java SE5之后,并发包中增加了lock接口,它提供了与synchronized一样的锁功能。虽然它失去了像synchronize关键字隐式加锁解锁的便捷性,但是却拥有了锁获取和释放的可操作性,可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。通常使用显示使用lock的形式如下:

1
2
3
4
5
6
7
Lock lock = new ReentrantLock();
lock.lock();
try{
.......
}finally{
lock.unlock();
}

需要注意的是synchronized同步块执行完成或者遇到异常是锁会自动释放,而lock必须调用unlock()方法释放锁,因此在finally块中释放锁

Lock接口API

我们现在就来看看lock接口定义了哪些方法:

void lock(); //获取锁 void lockInterruptibly() throws InterruptedException;//获取锁的过程能够响应中断 boolean tryLock();//非阻塞式响应中断能立即返回,获取锁放回true反之返回fasle boolean tryLock(long time, TimeUnit unit) throws InterruptedException;//超时获取锁,在超时内或者未中断的情况下能够获取锁 Condition newCondition();//获取与lock绑定的等待通知组件,当前线程必须获得了锁才能进行等待,进行等待时会先释放锁,当再次获取锁时才能从等待中返回

上面是lock接口下的五个方法,也只是从源码中英译中翻译了一遍,感兴趣的可以自己的去看看。那么在locks包下有哪些类实现了该接口了?先从最熟悉的ReentrantLock说起。

public class ReentrantLock implements Lock, java.io.Serializable

很显然ReentrantLock实现了lock接口,接下来我们来仔细研究一下它是怎样实现的。当你查看源码时你会惊讶的发现ReentrantLock并没有多少代码,另外有一个很明显的特点是:基本上所有的方法的实现实际上都是调用了其静态内存类Sync中的方法,而Sync类继承了AbstractQueuedSynchronizer(AQS)。可以看出要想理解ReentrantLock关键核心在于对队列同步器AbstractQueuedSynchronizer(简称同步器)的理解。

初识AQS

关于AQS在源码中有十分具体的解释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 Provides a framework for implementing blocking locks and related
synchronizers (semaphores, events, etc) that rely on
first-in-first-out (FIFO) wait queues. This class is designed to
be a useful basis for most kinds of synchronizers that rely on a
single atomic {@code int} value to represent state. Subclasses
must define the protected methods that change this state, and which
define what that state means in terms of this object being acquired
or released. Given these, the other methods in this class carry
out all queuing and blocking mechanics. Subclasses can maintain
other state fields, but only the atomically updated {@code int}
value manipulated using methods {@link #getState}, {@link
#setState} and {@link #compareAndSetState} is tracked with respect
to synchronization.
<p>Subclasses should be defined as non-public internal helper
classes that are used to implement the synchronization properties
of their enclosing class. Class
{@code AbstractQueuedSynchronizer} does not implement any
synchronization interface. Instead it defines methods such as
{@link #acquireInterruptibly} that can be invoked as
appropriate by concrete locks and related synchronizers to
implement their public methods.

同步器是用来构建锁和其他同步组件的基础框架,它的实现主要依赖一个int成员变量来表示同步状态以及通过一个FIFO队列构成等待队列。它的子类必须重写AQS的几个protected修饰的用来改变同步状态的方法,其他方法主要是实现了排队和阻塞机制。状态的更新使用getState,setState以及compareAndSetState这三个方法

子类被推荐定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态的获取和释放方法来供自定义同步组件的使用,同步器既支持独占式获取同步状态,也可以支持共享式获取同步状态,这样就可以方便的实现不同类型的同步组件。

同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。可以这样理解二者的关系:锁是面向使用者,它定义了使用者与锁交互的接口,隐藏了实现细节;同步器是面向锁的实现者,它简化了锁的实现方式,屏蔽了同步状态的管理,线程的排队,等待和唤醒等底层操作。锁和同步器很好的隔离了使用者和实现者所需关注的领域。

AQS的模板方法设计模式

AQS的设计是使用模板方法设计模式,它将一些方法开放给子类进行重写,而同步器给同步组件所提供模板方法又会重新调用被子类所重写的方法。举个例子,AQS中需要重写的方法tryAcquire:

1
2
3
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}

ReentrantLock中NonfairSync(继承AQS)会重写该方法为:

1
2
3
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}

而AQS中的模板方法acquire():

1
2
3
4
5
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

会调用tryAcquire方法,而此时当继承AQS的NonfairSync调用模板方法acquire时就会调用已经被NonfairSync重写的tryAcquire方法。这就是使用AQS的方式,在弄懂这点后会lock的实现理解有很大的提升。可以归纳总结为这么几点:

  1. 同步组件(这里不仅仅值锁,还包括CountDownLatch等)的实现依赖于同步器AQS,在同步组件实现中,使用AQS的方式被推荐定义继承AQS的静态内存类;
  2. AQS采用模板方法进行设计,AQS的protected修饰的方法需要由继承AQS的子类进行重写实现,当调用AQS的子类的方法时就会调用被重写的方法;
  3. AQS负责同步状态的管理,线程的排队,等待和唤醒这些底层操作,而Lock等同步组件主要专注于实现同步语义;
  4. 在重写AQS的方式时,使用AQS提供的getState(),setState(),compareAndSetState()方法进行修改同步状态

AQS可重写的方法如下图(摘自《java并发编程的艺术》一书):

AQS可重写的方法.pngAQS可重写的方法.png

在实现同步组件时AQS提供的模板方法如下图:

AQS提供的模板方法.pngAQS提供的模板方法.png

AQS提供的模板方法可以分为3类:

  1. 独占式获取与释放同步状态;
  2. 共享式获取与释放同步状态;
  3. 查询同步队列中等待线程情况;

同步组件通过AQS提供的模板方法实现自己的同步语义。

一个例子

下面使用一个例子来进一步理解下AQS的使用。这个例子也是来源于AQS源码中的example。

1
2
class Mutex implements Lock, java.io.Serializable {    // Our internal helper class    // 继承AQS的静态内存类    // 重写方法    private static class Sync extends AbstractQueuedSynchronizer {        // Reports whether in locked state        protected boolean isHeldExclusively() {            return getState() == 1;        }    // Acquires the lock if state is zero    public boolean tryAcquire(int acquires) {        assert acquires == 1; // Otherwise unused        if (compareAndSetState(0, 1)) {            setExclusiveOwnerThread(Thread.currentThread());            return true;        }        return false;    }     // Releases the lock by setting state to zero    protected boolean tryRelease(int releases) {        assert releases == 1; // Otherwise unused        if (getState() == 0) throw new IllegalMonitorStateException();        setExclusiveOwnerThread(null);        setState(0);        return true;    }     // Provides a Condition    Condition newCondition() {        return new ConditionObject();    }     // Deserializes properly    private void readObject(ObjectInputStream s)            throws IOException, ClassNotFoundException {        s.defaultReadObject();        setState(0); // reset to unlocked state    } } // The sync object does all the hard work. We just forward to it. private final Sync sync = new Sync(); //使用同步器的模板方法实现自己的同步语义 public void lock() {    sync.acquire(1); } public boolean tryLock() {    return sync.tryAcquire(1); } public void unlock() {    sync.release(1); } public Condition newCondition() {    return sync.newCondition(); } public boolean isLocked() {    return sync.isHeldExclusively(); } public boolean hasQueuedThreads() {    return sync.hasQueuedThreads(); } public void lockInterruptibly() throws InterruptedException {    sync.acquireInterruptibly(1); } public boolean tryLock(long timeout, TimeUnit unit)        throws InterruptedException {    return sync.tryAcquireNanos(1, unit.toNanos(timeout)); } 复制代码 复制代码
}

MutexDemo:

1
2
public class MutextDemo {    private static Mutex mutex = new Mutex(); public static void main(String[] args) {    for (int i = 0; i < 10; i++) {        Thread thread = new Thread(() -> {            mutex.lock();            try {                Thread.sleep(3000);            } catch (InterruptedException e) {                e.printStackTrace();            } finally {                mutex.unlock();            }        });        thread.start();    } } 复制代码 复制代码
}

执行情况:

mutex的执行情况.pngmutex的执行情况.png

上面的这个例子实现了独占锁的语义,在同一个时刻只允许一个线程占有锁。MutexDemo新建了10个线程,分别睡眠3s。从执行情况也可以看出来当前Thread-6正在执行占有锁而其他Thread-7,Thread-8等线程处于WAIT状态。按照推荐的方式,Mutex定义了一个继承AQS的静态内部类Sync,并且重写了AQS的tryAcquire等等方法,而对state的更新也是利用了setState(),getState(),compareAndSetState()这三个方法。在实现实现lock接口中的方法也只是调用了AQS提供的模板方法(因为Sync继承AQS)。从这个例子就可以很清楚的看出来,在同步组件的实现上主要是利用了AQS,而AQS“屏蔽”了同步状态的修改,线程排队等底层实现,通过AQS的模板方法可以很方便的给同步组件的实现者进行调用。而针对用户来说,只需要调用同步组件提供的方法来实现并发编程即可。同时在新建一个同步组件时需要把握的两个关键点是:

  1. 实现同步组件时推荐定义继承AQS的静态内存类,并重写需要的protected修饰的方法;
  2. 同步组件语义的实现依赖于AQS的模板方法,而AQS模板方法又依赖于被AQS的子类所重写的方法。

通俗点说,因为AQS整体设计思路采用模板方法设计模式,同步组件以及AQS的功能实际上别切分成各自的两部分:

同步组件实现者的角度:

通过可重写的方法:独占式: tryAcquire()(独占式获取同步状态),tryRelease()(独占式释放同步状态);共享式 :tryAcquireShared()(共享式获取同步状态),tryReleaseShared()(共享式释放同步状态);告诉AQS怎样判断当前同步状态是否成功获取或者是否成功释放。同步组件专注于对当前同步状态的逻辑判断,从而实现自己的同步语义。这句话比较抽象,举例来说,上面的Mutex例子中通过tryAcquire方法实现自己的同步语义,在该方法中如果当前同步状态为0(即该同步组件没被任何线程获取),当前线程可以获取同时将状态更改为1返回true,否则,该组件已经被线程占用返回false。很显然,该同步组件只能在同一时刻被线程占用,Mutex专注于获取释放的逻辑来实现自己想要表达的同步语义。

AQS的角度

而对AQS来说,只需要同步组件返回的true和false即可,因为AQS会对true和false会有不同的操作,true会认为当前线程获取同步组件成功直接返回,而false的话就AQS也会将当前线程插入同步队列等一系列的方法。

总的来说,同步组件通过重写AQS的方法实现自己想要表达的同步语义,而AQS只需要同步组件表达的true和false即可,AQS会针对true和false不同的情况做不同的处理,至于底层实现,可以看这篇文章


本文整理自

Java中的锁及AQS实现原理

初识Lock与AbstractQueuedSynchronizer(AQS)

仅做个人学习总结所用,遵循CC 4.0 BY-SA版权协议,如有侵权请联系删除!


很多介绍Java语言的书籍(包括《Java编程思想》)都对protected介绍的比较的简单,基本都是一句话,就是:被protected修饰的成员对于本包和其子类可见。这种说法有点太过含糊,常常会对大家造成误解。实际上,protected的可见性在于两点:

  • 基类的protected成员是包内可见的,并且对子类可见

  • 子类与基类不在同一包中,那么在子类中,子类实例可以访问其从基类继承而来的protected方法,而不能访问基类实例的protected方法

我们可以通过以下几个关于protected方法可见性的例子来进一步掌握protected关键字。在碰到涉及protected成员的调用时,首先要确定出该protected成员来自何方,其可见性范围是什么,然后就可以判断出当前用法是否可行了,看下面七个例子:

(1)、示例一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//示例一
package p1;
public class Father1 {
protected void f() {} // 父类Father1中的protected方法
}

package p1;
public class Son1 extends Father1 {}

package p11;
public class Son11 extends Father1{}

package p1;
public class Test1 {
public static void main(String[] args) {
Son1 son1 = new Son1();
son1.f(); // Compile OK ----(1)
son1.clone(); // Compile Error ----(2)

Son11 son = new Son11();
son11.f(); // Compile OK ----(3)
son11.clone(); // Compile Error ----(4)
}
}

  对于上面的示例,首先看(1)(3),其中的f()方法从类Father1继承而来,其可见性是包p1及其子类Son1和Son11,而由于调用f()方法的类Test1所在的包也是p1,因此(1)(3)处编译通过。其次看(2)(4),其中的clone()方法的可见性是java.lang包及其所有子类,对于语句“son1.clone();”和“son11.clone();”,二者的clone()在类Son1、Son11中是可见的,但对Test1是不可见的,因此(1)(3)处编译不通过。

(2)、示例二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//示例二
package p2;
class MyObject2 {
protected Object clone() throws CloneNotSupportedException{
return super.clone();
}
}

package p22;
public class Test2 extends MyObject2 {
public static void main(String args[]) {
MyObject2 obj = new MyObject2();
obj.clone(); // Compile Error ----(1)

Test2 tobj = new Test2();
tobj.clone(); // Complie OK ----(2)
}
}

  对于(1)而言,clone()方法来自于类MyObject2本身,因此其可见性为包p2及MyObject2的子类,虽然Test2是MyObject2的子类,但在Test2中不能访问基类MyObject2的protected方法clone(),因此编译不通过;对于(2)而言,由于在Test2中访问的是其本身实例的从基类MyObject2继承来的的clone(),因此编译通过。

(3)、示例三

1
2
3
4
5
6
7
8
9
10
11
12
//示例三
package p3;
class MyObject3 extends Test3 {
}

package p33;
public class Test3 {
public static void main(String args[]) {
MyObject3 obj = new MyObject3();
obj.clone(); // Compile OK ------(1)
}
}

  对于(1)而言,clone()方法来自于类Test3,因此其可见性为包p33及其子类MyObject3,而(1)正是在p33的类Test3中调用,属于同一包,编译通过。

(4)、示例四

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//示例四
package p4;
class MyObject4 extends Test4 {
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}

package p44;
public class Test4 {
public static void main(String args[]) {
MyObject4 obj = new MyObject4();
obj.clone(); // Compile Error -----(1)
}
}

  对于(1)而言,clone()方法来自于类MyObject4,因此其可见性为包p4及其子类(此处没有子类),而类Test4却在包p44中,因此不满足可见性,编译不通过。

(5)、示例五

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//示例五
package p5;

class MyObject5 {
protected Object clone() throws CloneNotSupportedException{
return super.clone();
}
}
public class Test5 {
public static void main(String[] args) throws CloneNotSupportedException {
MyObject5 obj = new MyObject5();
obj.clone(); // Compile OK ----(1)
}
}

  对于(1)而言,clone()方法来自于类MyObject5,因此其可见性为包p5及其子类(此处没有子类),而类Test5也在包p5中,因此满足可见性,编译通过。

(6)、示例六

1
2
3
4
5
6
7
8
9
10
//示例六
package p6;

class MyObject6 extends Test6{}
public class Test6 {
public static void main(String[] args) {
MyObject6 obj = new MyObject6();
obj.clone(); // Compile OK -------(1)
}
}

  对于(1)而言,clone()方法来自于类Test6,因此其可见性为包p6及其子类MyObject6,而类Test6也在包p6中,因此满足可见性,编译通过。

(7)、示例七

1
2
3
4
5
6
7
8
9
10
11
12
//示例七
package p7;

class MyObject7 extends Test7 {
public static void main(String[] args) {
Test7 test = new Test7();
test.clone(); // Compile Error ----- (1)
}
}

public class Test7 {
}

  对于(1)而言,clone()方法来自于类Object,因此该clone()方法可见性为包java.lang及其子类Test7,由于类MyObject7不在此范围内,因此不满足可见性,编译不通过。


本文整理自

Java protected 关键字详解

仅做个人学习总结所用,遵循CC 4.0 BY-SA版权协议,如有侵权请联系删除!


定义

里氏代换原则由2008年图灵奖得主、美国第一位计算机科学女博士Barbara Liskov教授和卡内基·梅隆大学Jeannette Wing教授于1994年提出。其严格表述如下:如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1代换o2时,程序P的行为没有变化,那么类型S是类型T的子类型。这个定义比较拗口且难以理解,因此我们一般使用它的另一个通俗版定义:

里氏代换原则(Liskov Substitution Principle, LSP):所有引用基类(父类)的地方必须能透明地使用其子类的对象。

说明

里氏代换原则告诉我们,在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。例如:我喜欢动物,那我一定喜欢狗,因为狗是动物的子类;但是我喜欢狗,不能据此断定我喜欢动物,因为我并不喜欢老鼠,虽然它也是动物。

例如有两个类,一个类为BaseClass,另一个是SubClass类,并且SubClass类是BaseClass类的子类,那么一个方法如果可以接受一个BaseClass类型的基类对象base的话,如:method1(base),那么它必然可以接受一个BaseClass类型的子类对象sub,method1(sub)能够正常运行。反过来的代换不成立,如一个方法method2接受BaseClass类型的子类对象sub为参数:method2(sub),那么一般而言不可以有method2(base),除非是重载方法。

开闭原则

里氏代换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象

在使用里氏代换原则时需要注意如下几个问题:

(1)子类的所有方法必须在父类中声明,或子类必须实现父类中声明的所有方法。根据里氏代换原则,为了保证系统的扩展性,在程序中通常使用父类来进行定义,如果一个方法只存在子类中,在父类中不提供相应的声明,则无法在以父类定义的对象中使用该方法。

(2) 我们在运用里氏代换原则时,尽量把父类设计为抽象类或者接口,让子类继承父类或实现父接口,并实现在父类中声明的方法,运行时,子类实例替换父类实例,我们可以很方便地扩展系统的功能,同时无须修改原有子类的代码,增加新的功能可以通过增加一个新的子类来实现。里氏代换原则是开闭原则的具体实现手段之一。

(3) Java语言中,在编译阶段,Java编译器会检查一个程序是否符合里氏代换原则,这是一个与实现无关的、纯语法意义上的检查,但Java编译器的检查是有局限的。

里氏代换原则是实现开闭原则的重要方式之一。在传递参数时使用基类对象,除此以外,在定义成员变量、定义局部变量、确定方法返回类型时都可使用里氏代换原则。针对基类编程,在程序运行时再确定具体子类。

推论

  • 子类在重写(Override)父类方法时,子类方法的访问修饰符不能比父类更加严格.

比如父类方法是包访问权限,子类的重写方法是public访问权限

  • 子类在重写(Override)父类方法时,子类方法抛出的异常不能比父类更多.

子类重写方法可以不抛异常以及抛出更少的异常

\扩展**

里氏代换原则以Barbara Liskov(芭芭拉·利斯科夫)教授的姓氏命名。芭芭拉·利斯科夫:美国计算机科学家,2008年图灵奖得主,2004年约翰·冯诺依曼奖得主,美国工程院院士,美国艺术与科学院院士,美国计算机协会会士,麻省理工学院电子电气与计算机科学系教授,美国第一位计算机科学女博士。


本文整理自

里氏代换 – 子类可以替换父类

仅做个人学习总结所用,遵循CC 4.0 BY-SA版权协议,如有侵权请联系删除!


子类不能重写父类的静态方法,私有方法。即使你看到子类中存在貌似是重写的父类的静态方法或者私有方法,编译是没有问题的,但那其实是你重新又定义的方法,不是重写。具体有关重写父类方法的规则如下:

重写规则之一:

重写方法不能比被重写方法限制有更严格的访问级别。

因为需要保证:任何父类出现的地方,替换成子类依然可以使用.

访问权限可以更广泛,比如父类方法是包访问权限,子类的重写方法是public访问权限。有个人曾经这样说:父类为protected的,子类重写时一定要用public;我觉得这个观点不能是绝对的,只要满足子类的访问权限不比父类低就行了。

比如:Object类有个toString()方法,开始重写这个方法的时候我们总容易忘记public修饰符,编译器会报错。出错的原因就是:没有加任何访问修饰符的方法具有包访问权限,包访问权限比public当然要严格了,所以编译器会报错的。

重写规则之二:

参数列表必须与被重写方法的相同。

重写有个孪生的弟弟叫重载,也就是后面要出场的。如果子类方法的参数与父类对应的方法不同,那么就是你认错人了,那是重载,不是重写。

重写规则之三:

返回类型必须与被重写方法的返回类型相同。

父类方法A:void eat(){} 子类方法B:int eat(){}两者虽然参数相同,可是返回类型不同,所以不是重写。

父类方法A:int eat(){} 子类方法B:long eat(){}返回类型虽然兼容父类,但是不同就是不同,所以不是重写。

重写规则之四:

重写方法不能抛出新的异常或者比被重写方法声明的检查异常更广的检查异常。但是可以抛出更少,更有限或者不抛出异常。

注意:这种限制只是针对检查异常,至于运行时异常RuntimeException及其子类不在这个限制之中

重写规则之五:

不能重写被标识为final的方法。

重写规则之六:

如果一个方法不能被继承,则不能重写它。如private方法

比较典型的就是父类的private方法。下例会产生一个有趣的现象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Test {  
public static void main (String[] args) {
//Animal h = new Horse();
Horse h = new Horse();
h.eat();
}
}

class Animal {
private void eat(){
System.out.println ("Animal is eating.");
}
}

class Horse extends Animal{
public void eat(){
System.out.println ("Horse is eating.");
}
}

这段代码是能通过编译的。表面上看来违反了第六条规则,但实际上那是一点巧合。Animal类的eat()方法不能被继承,因此Horse类中的eat()方法是一个全新的方法,不是重写也不是重载,只是一个只属于Horse类的全新的方法!这点让很多人迷惑了,但是也不是那么难以理解。

main()方法如果是这样:

1
2
3
Animal h = new Horse();
//Horse h = new Horse();
h.eat();

编译器会报错,为什么呢?Horse类的eat()方法是public的啊!应该可以调用啊!请牢记,多态只看父类引用的方法,而不看子类对象的方法!

重写规则之七:

子类不能用 静态方法重写父类的非静态方法

编绎无法通过this static method cannot hide the instance mehtod from A

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A {  
protected int method1(int a, int b) {
return 0;
}
}

public class Test1 extends A {

private int method1(int a, long b) {
return 0;
}
//this static method cannot hide the instance mehtod from A
static public int method1(int a, int b) {
return 0;
}
}

重写规则之八:

子类不能用非静态方法重写父类的静态方法

编绎报错:this instance method cannot override the static mehtod from A

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A {  
protected static int method1(int a, int b) {
return 0;
}
}

public class Test1 extends A {
private int method1(int a, long b) {
return 0;
}
//this static method cannot hide the instance mehtod from A
//this instance method cannot override the static mehtod from A
public int method1(int a, int b) {
return 0;
}
}

本文整理自

Java子类重写父类方法注意问题

仅做个人学习总结所用,遵循CC 4.0 BY-SA版权协议,如有侵权请联系删除!


Java的4种访问控制修饰符

Java使用访问控制修饰符(Access Modifiers)来保护对类、变量、方法和构造方法的访问。

  • 共有的,以public 修饰符指定,对所有类可见。
  • 受保护的,以 protected 修饰符指定,对同一包内的类和所有子类可见。
  • 默认的,也称为 default,在同一包内可见,不使用任何修饰符(No Keyword)。
  • 私有的,以 private 修饰符指定,在同一类内可见。

Java provides a number of access modifiers to set access levels
for classes, variables, methods, and constructors. The four access
levels are

  • Visible to the package, the default. No modifiers are needed.
  • Visible to the class only (private).
  • Visible to the world (public).
  • Visible to the package and all subclasses (protected).

可以采用以下表格形式描述方便大家记忆:

访问修饰符 类内部 当前包 子类 其他包
public
protected ×
default × ×
private × × ×

继承规则中的访问控制

方法的继承

java

Java中子类继承父类方法时,有如下规则:

  • 父类或超类中的public方法,在子类中也必须是public。
  • 父类中的protected方法,在子类中必须是protected或public,不能为private。
  • 父类中private的方法不能被继承。

Access Control and Inheritance

The following rules for inherited methods are enforced.

  • Methods declared public in a superclass also must be public in all subclasses.
  • Methods declared protected in a superclass must either be protected or public in subclasses; they cannot be private.
  • Methods declared private are not inherited at all, so there is no rule for them.

属性的继承

另外,对于子类继承父类属性(成员变量)需注意:

  • 从父类继承的成员变量,其访问控制符仍然相同。

  • 子类定义与父类同名的成员变量,并没有覆盖父类的成员变量,而是两个成员变量共存;默认情况下,父类的成员变量是被隐藏的,如果你非要调用父类的成员变量可使用super关键字。

静态属性方法的继承

而对于静态属性(成员变量)与静态方法:

  • 静态属性和静态方法可以被继承,但是不是被重写(override)而是被隐藏。这是因为静态方法和属性是属于类的,调用的时候直接通过类名.方法名完成对,不需要继承机制及可以调用。

  • 静态属性、静态方法和非静态的属性都可以被继承和隐藏而不能被重写,因此不能实现多态,不能实现父类的引用可以指向不同子类的对象;非静态方法可以被继承和重写,因此可以实现多态。

    构造方法在继承关系下的调用

    最后还有一点,请注意:

  • Java继承中对构造函数是不继承的,只是隐式或显式的调用(显而易见,构造函数命名与类同名,子类和父类不可能同名,也就谈不上继承覆盖)。
    Java中创建类的对象时,如果该类存在父类,则先调用父类的构造方法,然后再调用子类的构造方法。

  • 如果父类没有定义构造方法,则调用编译器自动创建的不带参数的默认构造方法。(如果没有任何构造方法,系统会默认有一个无参构造方法)

  • 如果父类定义了public的无参的构造方法,则在调用子类的构造方法前会自动先调用该无参的构造方法。

  • 如果父类只有有参的构造方法,没有无参的构造方法,则子类必须在构造方法中必须显式调用super(参数列表)来指定某个有参的构造方法。(因为创建有参构造方法后,系统就不再有默认无参构造方法了)

  • 如果父类定义了无参的构造方法,也有有参的构造方法,则子类可以指定调用某个构造方法,如果没有指定,则调用无参构造方法。

  • 如果父类定义有无参的构造方法,但无参的构造方法声明为private,则子类同样必须在构造方法中必须显式调用super(参数列表)来指定某个有参的构造方法。

  • 如果父类定义有无参的构造方法,但无参的构造方法声明为private,而且没有其他的有参构造方法,则子类无法创建。


本文整理自

Java访问控制修饰符与继承

仅做个人学习总结所用,遵循CC 4.0 BY-SA版权协议,如有侵权请联系删除!


前言

object clone(对象克隆)网上资料很多,那我为什么还要写下这篇文章呢?主要是想汇聚多篇文章的优秀之处以及我对于对象克隆的理解来加深印象,也使读者能更全面的理解对象克隆的用法、原理和用途。

何谓 “object clone”

顾名思义clone就是一个相同东西的副本,是一个具体存在的复制体,是一个从生物科学开始变得熟悉的术语。在计算机行业,该术语被广泛用于指Compaq,戴尔等人对IBM PC的模仿。而在java语言中,clone方法被对象调用,所以会复制对象。

clone的用法

(1)方法摘要

作用域 类型 方法 描述
protected Object clone() 克隆实现了Cloneable接口的对象

注意事项:clone方法是被native修饰的,简单的讲就是被Native修饰的方法在被调用时指向的是一个非java代码的具体实现,这个实现可能是其他语言或者操作系统。

(2)clone规则:

1
2
3
4
5
6
1、 基本类型  
如果变量是基本类型,则拷贝其值,比如int、float等。
2、 对象
如果变量是一个实例对象,则拷贝其地址引用,也就是说新对象和原来对象是共用实例变量的。
3、 String字符串
若变量为String字符串,则拷贝其地址引用。但是在修改时,它会从字符串池中重新生成一个新的字符串,原有的对象保持不变。复制代码

(2)示例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
实现clone方法的步骤:
1. 实现Cloneable接口
2. 重载Object类中的clone()方法,重载时需定义为public
3. 在重载方法中,调用super.clone()复制代码
public class Book implements Cloneable {

private int id;

private String name;

public Book() {}

public Book(int id, String name) {
this.id = id;
this.name = name;
}

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

@Override
public Object clone() throws CloneNotSupportedException {
return (Book)super.clone();
}

public static void main(String[] args) throws CloneNotSupportedException {
Book book1 = new Book();
book1.setName("基础系列1");
Book book2 = (Book) book1.clone();

System.out.println("图书1:" + book1.getName());
System.out.println("图书2:" + book2.getName());

book2.setName("基础系列2");

System.out.println("图书1:" + book1.getName());
System.out.println("图书2:" + book2.getName());

}
}

运行结果:

1
2
3
4
图书1:基础系列1
图书2:基础系列1
图书1:基础系列1
图书2:基础系列2复制代码

从运行结果看这应该是深克隆的,但为什么是浅克隆呢?从string不可变(原对象和克隆对象中的string属性引用的是同一地址)的角度出发结果应该是浅克隆,但从结果出发却又是深克隆,所以从这一角度来说clone对string是深克隆。

注意事项:如果没有implements Cloneable的类调用Object.clone()方法就会抛出CloneNotSupportedException

(3)示例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
public class Book implements Cloneable {

//在示例1的基础上增加bookBorrow的引用
private BookBorrow bookBorrow;

public Book() {}

public Book(int id, String name, BookBorrow bookBorrow) {
this.id = id;
this.name = name;
this.bookBorrow = bookBorrow;
}

public BookBorrow getBookBorrow() {
return bookBorrow;
}

public void setBookBorrow(BookBorrow bookBorrow) {
this.bookBorrow = bookBorrow;
}

@Override
public Object clone() throws CloneNotSupportedException {
Book book = (Book)super.clone();
//这里注释掉就是浅克隆,否则就是深克隆
book.bookBorrow = (BookBorrow)bookBorrow.clone();
return book;
}

@Override
public String toString() {
return "BOOK[id="+id+",name="+name+",bookBorrow:"+bookBorrow+"]";
}

public static void main(String[] args) throws CloneNotSupportedException {

BookBorrow bookBorrow = new BookBorrow(1,1);
Book book1 = new Book(1,"基础系列1",bookBorrow);
Book book2 = (Book) book1.clone();

System.out.println("图书1:" + book1.toString());
System.out.println("图书2:" + book2.toString());

book2.setName("基础系列2");
book2.setBookBorrow(new BookBorrow(5,5));

System.out.println("图书1:" + book1.toString());
System.out.println("图书2:" + book2.toString());

}
}

public class BookBorrow implements Cloneable{

private int id;

private int borstate;


public BookBorrow(int id, int borstate) {
this.id = id;
this.borstate = borstate;
}

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public int getBorstate() {
return borstate;
}

public void setBorstate(int borstate) {
this.borstate = borstate;
}

@Override
public Object clone() throws CloneNotSupportedException {
return (BookBorrow)super.clone();
}

@Override
public String toString() {
return "BookBorrow[id="+id+",borstate="+borstate+"]";
}

}

运行结果:

1
2
3
4
图书1:BOOK[id=1,name=基础系列1,bookBorrow:BookBorrow[id=1,borstate=1]]
图书2:BOOK[id=1,name=基础系列1,bookBorrow:BookBorrow[id=1,borstate=1]]
图书1:BOOK[id=1,name=基础系列1,bookBorrow:BookBorrow[id=1,borstate=1]]
图书2:BOOK[id=1,name=基础系列2,bookBorrow:BookBorrow[id=5,borstate=5]]复制代码

从结果看这里是一个标准的深克隆实现,深克隆实现的一个主要前提是当前对象引用的对象或对象的对象引用的对象都实现了常规用法1并且在重载clone方法中调用其引用对象的clone方法。

例:

1
2
3
4
5
6
7
@Override
public Object clone() throws CloneNotSupportedException {
Book book = (Book)super.clone();
//这里注释掉就是浅克隆,否则就是深克隆
book.bookBorrow = (BookBorrow)bookBorrow.clone();
return book;
}

注意事项:示例2给出的例子是相对简单且常见的类,在实际开发中clone的对象可能依赖第三方的jar包或者引用层级过深不好修改的对象,如果是这种情况则建议采用示例3的做法,使用序列化clone。

(3)示例3:

序列化clone类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class CloneUtils {

public static <T extends Serializable> T clone(T obj){

T cloneObj = null;
try {
//写入字节流
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream obs = new ObjectOutputStream(out);
obs.writeObject(obj);
obs.close();

//分配内存,写入原始对象,生成新对象
ByteArrayInputStream ios = new ByteArrayInputStream(out.toByteArray());
ObjectInputStream ois = new ObjectInputStream(ios);
//返回生成的新对象
cloneObj = (T) ois.readObject();
ois.close();
} catch (Exception e) {
e.printStackTrace();
}

return cloneObj;
}
}

public class BookBorrow implements Serializable{
...
//去掉clone方法,继承Serializable

}

public class Book implements Serializable {
...
//去掉clone方法,继承Serializable

public static void main(String[] args) throws CloneNotSupportedException {

BookBorrow bookBorrow = new BookBorrow(1,1);
Book book1 = new Book(1,"基础系列1",bookBorrow);
Book book2 = CloneUtils.clone(book1);

System.out.println("图书1:" + book1.toString());
System.out.println("图书2:" + book2.toString());

book2.setName("基础系列2");
book2.setBookBorrow(new BookBorrow(5,5));

System.out.println("图书1:" + book1.toString());
System.out.println("图书2:" + book2.toString());

}
}

执行结果:

1
2
3
4
图书1:BOOK[id=1,name=基础系列1,bookBorrow:BookBorrow[id=1,borstate=1]]
图书2:BOOK[id=1,name=基础系列1,bookBorrow:BookBorrow[id=1,borstate=1]]
图书1:BOOK[id=1,name=基础系列1,bookBorrow:BookBorrow[id=1,borstate=1]]
图书2:BOOK[id=1,name=基础系列2,bookBorrow:BookBorrow[id=5,borstate=5]]

序列化克隆无需继承,通过序列化工具类可实现深克隆同等效果。然而序列化这种方式在效率上不如clone

clone原理

本次讲解将基于示例1做出解释:

为了不丢失上下文而贴出的测试代码,将会以2部分讲解object clone的原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) throws CloneNotSupportedException {
//第一部分
Book book1 = new Book();
book1.setName("基础系列1");
Book book2 = (Book) book1.clone();

System.out.println("图书1:" + book1.getName());
System.out.println("图书2:" + book2.getName());

//第二部分
book2.setName("基础系列2");

System.out.println("图书1:" + book1.getName());
System.out.println("图书2:" + book2.getName());

}

第一部分执行结果

1
2
图书1:基础系列1
图书2:基础系列1

浅克隆原理图:

image

从图中可以看出clone的name引用的是同一个值,那为什么前面又说是深克隆呢?原因就是在这一步中并没有修改name所以他们是浅克隆,引用的是同一个name变量值。那接下来执行第二部分得出的结果和原理图如你所想对象完全隔离了。

第二部分执行结果

1
2
图书1:基础系列1
图书2:基础系列2

深克隆原理图:

image

从图可以看出修改了name属性值,clone会从堆中重新生成一个对象被克隆对象引用,而原对象保持不变,从这一角度出发的确是深克隆。

clone原理小结

前面的原理介绍是以示例1做为蓝本介绍的,示例2 的原理和示例1类似,唯一区别是多了属性对象而属性对象在clone中也只会拷贝引用地址,要想实现深克隆就只能在引用的对象或引用对象的对象中中添加clone方法实现即可实现深克隆。

clone的实际用途

1、精心设计一个浅克隆对象被程序缓存,作为功能模块模板;每次有用户调用这个模块则将可变部分替换成用户需要的信息即可。
示例:
功能:发邮件
描述:给同组的用户发送邮件,邮件内容相同(不可变)发送的用户不同(可变)

2、精心设计一个深克隆对象本程序缓存,作为功能模块的初始对象,例如:“游客模式”每个游客进入系统访问的都是初始对象,基于初始对象发展出多条变化不一的游览路线。只要你想的到设计巧妙,很多功能都能应用object clone。

总结

本文分3部分介绍了object clone,分别介绍了clone的用法、原理和用途; object clone归结就是可变和不可变两个特性,在实际的开发中我们可以基于这2个特性设计出性能良好的功能模块。


本文整理自

(基础系列)object clone 的用法、原理和用途

仅做个人学习总结所用,遵循CC 4.0 BY-SA版权协议,如有侵权请联系删除!


Header

List 集合中,之前分析了 ArrayList ,还剩下了 LinkedList 没有分析过。那么趁着今天有空,就把 LinkedList 的内部原理来讲讲吧。

LinkedList 是有序并且可以元素重复的集合,底层是基于双向链表的。也正因为是链表,所以也就没有动态扩容的步骤了。

源码分析

构造方法

1
2
3
4
5
6
7
public LinkedList() {
}

public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}

构造方法一个是默认的,另外一个是传入一个集合,然后调用 addAll 方法添加集合所有的元素。

Node

LinkedList 既然作为链表,那么肯定会有节点了,我们看下节点的定义:

1
2
3
4
5
6
7
8
9
10
11
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;

Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}

每个节点都包含了前一个节点 prev 以及后一个节点 next ,item 就是要当前节点要存储的元素。

add(E e)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public boolean add(E e) {
// 直接往队尾加元素
linkLast(e);
return true;
}

void linkLast(E e) {
// 保存原来链表尾部节点,last 是全局变量,用来表示队尾元素
final Node<E> l = last;
// 为该元素 e 新建一个节点
final Node<E> newNode = new Node<>(l, e, null);
// 将新节点设为队尾
last = newNode;
// 如果原来的队尾元素为空,那么说明原来的整个列表是空的,就把新节点赋值给头结点
if (l == null)
first = newNode;
else
// 原来尾结点的后面为新生成的结点
l.next = newNode;
// 节点数 +1
size++;
modCount++;
}

linkLast(E e) 中,先去判断了原来的尾节点是否为空。如果尾节点是空的,那么就说明原来的列表是空的。会将头节点也指向该元素;如果不为空,直接在后面追加即可。

其实在 first 之前,还有一个为 null 的 head 节点。head 节点的 next 才是 first 节点。

add(int index, E element)

1
2
3
4
5
6
7
8
9
10
public void add(int index, E element) {
// 检查 index 有没有超出索引范围
checkPositionIndex(index);
// 如果追加到尾部,那么就跟 add(E e) 一样了
if (index == size)
linkLast(element);
else
// 否则就是插在其他位置
linkBefore(element, node(index));
}

add(int index, E element) 中主要就看 linkBefore(element, node(index)) 方法了。注意到有一个 node(index) ,好奇究竟做了什么操作?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Node<E> node(int index) {
// assert isElementIndex(index);
// 如果 index 在前半段,从前往后遍历获取 node
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
// 如果 index 在后半段,从后往前遍历获取 node
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}

原来是为了索引得到 index 对应的节点,在速度上做了算法优化。

得到 Node 后,就会去调用 linkBefore(element, node)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
// 保存 index 节点的前节点
final Node<E> pred = succ.prev;
// 新建一个目标节点
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
// 如果是在开头处插入的话
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}

这段代码和之前的很类似,了解链表节点插入的同学对这段代码应该很 easy 了。

addAll(Collection<? extends E> c)

1
2
3
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}

addAll(Collection c) 内部直接调用的是 addAll(int index, Collection c)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public boolean addAll(int index, Collection<? extends E> c) {
// index 索引范围判断
checkPositionIndex(index);

Object[] a = c.toArray();
int numNew = a.length;
if (numNew == 0)
return false;

// 保存之前的前节点和后节点
Node<E> pred, succ;
// 判断是在尾部插入还是在其他位置插入
if (index == size) {
succ = null;
pred = last;
} else {
succ = node(index);
pred = succ.prev;
}

for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
Node<E> newNode = new Node<>(pred, e, null);
// 如果前节点是空的,就说明是在头部插入了
if (pred == null)
first = newNode;
else
pred.next = newNode;
pred = newNode;
}

if (succ == null) {
last = pred;
} else {
pred.next = succ;
succ.prev = pred;
}

size += numNew;
modCount++;
return true;
}

addAll(int index, Collection c) 其实就是相当于多次进行 add(int index, E element) 操作,在内部循环添加到链表上。

get(int index)

1
2
3
4
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}

在内部调用了 node(index) 方法,而 node(index) 方法在上面已经分析过了。就是判断在前半段还是在后半段,然后遍历得到即可。

remove(int index)

1
2
3
4
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}

remove(int index) 中调用了 unlink(Node x) 方法来移除该节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
// 如果要删除的是头节点,那么设置头节点为下一个节点
if (prev == null) {
first = next;
} else {
// 设置该节点的前节点的 next 为该节点的 next
prev.next = next;
x.prev = null;
}
// 如果要删除的是尾节点,那么设置尾节点为上一个节点
if (next == null) {
last = prev;
} else {
// 设置该节点的下一个节点的 prev 为该节点的 prev
next.prev = prev;
x.next = null;
}
// 设置 null 值,size--
x.item = null;
size--;
modCount++;
return element;
}

remove(Object o)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}

remove(Object o) 的代码就是遍历链表,然后得到相等的值就把它 unlink(x) 了。至于 unlink(Node x) 的代码,上面已经分析过啦。

set(int index, E element)

1
2
3
4
5
6
7
8
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
// 设置 x 节点的值为新值,然后返回旧值
x.item = element;
return oldVal;
}

clear()

1
2
3
4
5
6
7
8
9
10
11
12
13
public void clear() {
// 遍历链表,然后一一删除置空
for (Node<E> x = first; x != null; ) {
Node<E> next = x.next;
x.item = null;
x.next = null;
x.prev = null;
x = next;
}
first = last = null;
size = 0;
modCount++;
}

Footer

LinkedList 相对于 ArrayList 来说,源码会复杂一点。因为涉及到了链表,所以会有 prev 和 next 之分。


本文整理自

LinkedList内部原理解析

仅做个人学习总结所用,遵循CC 4.0 BY-SA版权协议,如有侵权请联系删除!


简介

ArrayList 是一种变长的基于数组实现的集合类,ArrayList 允许空值和重复元素,当往 ArrayList 中添加的元素数量大于其底层数组容量时,它会自动扩容至一个更大的数组。

另外,由于 ArrayList 底层基于数组实现,所以其可以保证在 O(1) 复杂度下完成随机查找操作。其他方面,ArrayList 是非线程安全类,并发环境下,多个线程同时操作 ArrayList,会引发不可预知的错误。

ArrayList 是大家最为常用的集合类,我们先来看下常用的方法:

1
2
3
4
5
6
List<String> dataList = new ArrayList<>();//创建 ArrayList
dataList.add("test");//添加数据
dataList.add(1,"test1");//指定位置,添加数据
dataList.get(0);//获取指定位置的数据
dataList.remove(0);//移除指定位置的数据
dataList.clear();//清空数据

构造方法

ArrayList 有两个构造方法,一个是无参,另一个需传入初始容量值。大家平时最常用的是无参构造方法,相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static final int DEFAULT_CAPACITY = 10; // 初始容量为 10
private static final Object[] EMPTY_ELEMENTDATA = {};// 一个空对象
// 一个空对象,如果使用默认构造函数创建,则默认对象内容默认是该值
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData; //当前数据对象存放地方,当前对象不参与序列化
private int size; // 当前数组长度

public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}

public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

上面的代码比较简单,两个构造方法做的事情并不复杂,目的都是初始化底层数组 elementData。区别在于无参构造方法会将 elementData 初始化一个空数组,插入元素时,扩容将会按默认值重新初始化数组。而有参的构造方法则会将 elementData 初始化为参数值大小(>= 0)的数组。

add()

对于数组(线性表)结构,插入操作分为两种情况。一种是在元素序列尾部插入,另一种是在元素序列其他位置插入。

  • 尾部插入元素
1
2
3
4
5
6
7
8
/** 在元素序列尾部插入 */
public boolean add(E e) {
// 1. 检测是否需要扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
// 2. 将新元素插入序列尾部
elementData[size++] = e;
return true;
}

对于在元素序列尾部插入,这种情况比较简单,只需两个步骤即可:

  1. 检测数组是否有足够的空间插入
  2. 将新元素插入至序列尾部

如下图:

img

  • 指定位置插入元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/** 在元素序列 index 位置处插入 */
public void add(int index, E element) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

// 1. 检测是否需要扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
// 2. 将 index 及其之后的所有元素都向后移一位
// arraycopy(被复制的数组, 从第几个元素开始, 复制到哪里, 从第几个元素开始粘贴, 复制的元素个数)
System.arraycopy(elementData, index, elementData, index + 1, size - index);
// 3. 将新元素插入至 index 处
elementData[index] = element;
size++;
}

如果是在元素序列指定位置(假设该位置合理)插入,则情况稍微复杂一点,需要三个步骤:

  1. 检测数组是否有足够的空间
  2. 将 index 及其之后的所有元素向后移一位
  3. 将新元素插入至 index 处

如下图:

img

从上图可以看出,将新元素插入至序列指定位置,需要先将该位置及其之后的元素都向后移动一位,为新元素腾出位置。这个操作的时间复杂度为O(N),频繁移动元素可能会导致效率问题,特别是集合中元素数量较多时。在日常开发中,若非所需,我们应当尽量避免在大集合中调用第二个插入方法。

扩容机制

下面就来简单分析一下 ArrayList 的扩容机制,对于变长数据结构,当结构中没有空余空间可供使用时,就需要进行扩容。在 ArrayList 中,当空间用完,其会按照原数组空间的 1.5 倍进行扩容。相关源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/** 计算最小容量 */
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}

ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
modCount++;

// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}

/** 扩容的核心方法 */
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// newCapacity = oldCapacity + oldCapacity / 2 = oldCapacity * 1.5
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 进行扩容
elementData = Arrays.copyOf(elementData, newCapacity);
}

private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
// 如果最小容量超过 MAX_ARRAY_SIZE,则将数组容量扩容至 Integer.MAX_VALUE
return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}

上面就是扩容的逻辑,逻辑很简单,这里就不赘述了。

get()

1
2
3
4
5
6
public E get(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

return (E) elementData[index];
}

get 的逻辑很简单,就是检查是否越界,根据 index 获取元素。

remove()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public E remove(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

modCount++;
// 返回被删除的元素值
E oldValue = (E) elementData[index];

int numMoved = size - index - 1;
if (numMoved > 0)
// 将 index + 1 及之后的元素向前移动一位,覆盖被删除值
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 将最后一个元素置空,并将 size 值减 1
elementData[--size] = null; // clear to let GC do its work

return oldValue;
}

E elementData(int index) {
return (E) elementData[index];
}

/** 删除指定元素,若元素重复,则只删除下标最小的元素 */
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
// 遍历数组,查找要删除元素的位置
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}

/** 快速删除,不做边界检查,也不返回删除的元素值 */
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}

上面的删除方法并不复杂,这里以第一个删除方法为例,删除一个元素步骤如下:

  1. 获取指定位置 index 处的元素值
  2. 将 index + 1 及之后的元素向前移动一位
  3. 将最后一个元素置空,并将 size 值减 1
  4. 返回被删除值,完成删除操作

如下图:

img

上面就是删除指定位置元素的分析,并不是很复杂。

现在,考虑这样一种情况。我们往 ArrayList 插入大量元素后,又删除很多元素,此时底层数组会空闲处大量的空间。因为 ArrayList 没有自动缩容机制,导致底层数组大量的空闲空间不能被释放,造成浪费。对于这种情况,ArrayList 也提供了相应的处理方法,如下:

1
2
3
4
5
6
7
8
9
/** 将数组容量缩小至元素数量 */
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}

通过上面的方法,我们可以手动触发 ArrayList 的缩容机制。这样就可以释放多余的空间,提高空间利用率。

img

clear()

1
2
3
4
5
6
7
8
9
public void clear() {
modCount++;

// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;

size = 0;
}

clear 的逻辑很简单,就是遍历一下将所有的元素设置为空。


本文整理自

源码分析ArrayList原理

仅做个人学习总结所用,遵循CC 4.0 BY-SA版权协议,如有侵权请联系删除!