Java 面试宝典
第一章 内容介绍 20
第二章 JavaSE 基础 21
一、 Java 面向对象 21
1.面向对象都有哪些特性以及你对这些特性的理解 21
2.访问权限修饰符 public、private、protected, 以及不写(默认)时的区别(2017-11-12) 22
3.如何理解 clone 对象 22
二、 JavaSE 语法(2017-11-12-wl) 27
- Java 有没有 goto 语句?(2017-11-12-wl) 27
- & 和 && 的 区 别(2017-11-12-wl) 27
3.在 Java 中,如何跳出当前的多重嵌套循环(2017-11-14-wl) 27
4.两个对象值相同 (x.equals(y) == true) ,但却可有不同的 hashCode,这句话对不对?(2017-11-14- wl) 28 - 是否可以继承 String (2017-11-14-wl) 28
6.当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递?(2017-11-14-wl) 29
7.重载(overload)和重写(override)的区别?重载的方法能否根据返回类型进行区分?(2017-11-15- wl) 29
8.为什么函数不能根据返回类型来区分重载?(2017-11-15-wl) 30
9.char 型变量中能不能存储一个中文汉字,为什么?(2017-11-16-wl) 31
10.抽象类(abstract class)和接口(interface)有什么异同?(2017-11-16-wl) 31
11.抽象的(abstract)方法是否可同时是静态的(static), 是否可同时是本地方法(native),是否可同时被
synchronized(2017-11-16-wl) 32 - 阐述静态变量和实例变量的区别?(2017-11-16-wl) 32
- ==和 equals 的区别?(2017-11-22-wzz) 33
14.break 和 continue 的区别?(2017-11-23-wzz) 33
15.String s = “Hello”;s = s + " world!";这两行代码执行后,原始的 String 对象中的内容到底变了没有?
(2017-12-1-lyq) 33
三、 Java 中的多态 35 - Java 中实现多态的机制是什么? 35
四、 Java 的异常处理 35
1.Java 中异常分为哪些种类 35
2.调用下面的方法,得到的返回值是什么? 35
3.error 和 exception 的区别?(2017-2-23) 36 - java 异常处理机制(2017-2-23) 37
5.请写出你最常见的 5 个 RuntimeException(2017-11-22-wzz) 37
6.throw 和 throws 的区别(2017-11-22-wzz) 38
7.final、finally、finalize 的区别?(2017-11-23-wzz) 38
五、 JavaSE 常用 API 39 - Math.round(11.5)等于多少?Math.round(- 11.5) 又等于多少?(2017-11-14-wl) 39
2.switch 是否能作用在 byte 上,是否能作用在 long 上,是否能作用在 String 上?(2017-11-14-wl) 39
3.数组有没有 length() 方法?String 有没有 length() 方法?(2017-11-14-wl) 39
4.String 、StringBuilder 、StringBuffer 的区别?(2017-11-14-wl) 39
5.什么情况下用“+”运算符进行字符串连接比调用 StringBuffer/StringBuilder 对象的 append 方法连接字符串性能更好?(2017-11-14-wl) 40 - 请说出下面程序的输出(2017-11-14-wl) 47
- Java 中的日期和时间(2017-11-19-wl) 48
六、 Java 的数据类型 70 - Java 的基本数据类型都有哪些各占几个字节 70
- String 是基本数据类型吗?(2017-11-12-wl) 71
- short s1 = 1; s1 = s1 + 1; 有错吗?short s1 = 1; s1 += 1 有错吗;(2017-11-12-wl) 71
4.int 和 和 Integer 有什么区别?(2017-11-12-wl) 71
5.下面 Integer 类型的数值比较输出的结果为?(2017-11-12-wl) 72 - String 类常用方法(2017-11-15-lyq) 74
- String、StringBuffer、StringBuilder 的区别?(2017-11-23-wzz) 74
- 数据类型之间的转换(2017-11-23-wzz) 75
七 、 Java 的 IO 75 - Java 中有几种类型的流(2017-11-23-wzz) 75
2.字节流如何转为字符流 76
3.如何将一个 java 对象序列化到文件里 76 - 字节流和字符流的区别(2017-11-23-wzz) 77
- 如何实现对象克隆?(2017-11-12-wl) 77
- 什么是 java 序列化,如何实现 java 序列化?(2017-12-7-lyq) 80
八、 Java 的集合 81
1.HashMap 排序题,上机题。(本人主要靠这道题入职的第一家公司) 81
2.集合的安全性问题 83
3.ArrayList 内部用什么实现的?(2015-11-24) 83 - 并发集合和普通集合如何区别?(2015-11-24) 89
- List 的三个子类的特点(2017-2-23) 91
6.List 和 Map、Set 的区别(2017-11-22-wzz) 91
7.HashMap 和 HashTable 有什么区别?(2017-2-23) 92
8.数组和链表分别比较适合用于什么场景,为什么?(2017-2-23) 93
9.Java 中 ArrayList 和 Linkedlist 区 别?(2017-2-23) 96
10.List a=new ArrayList()和 ArrayList a =new ArrayList()的区别?(2017-2-24) 97
11.要对集合更新操作时,ArrayList 和 LinkedList 哪个更适合?(2017-2-24) 97 - 请用两个队列模拟堆栈结构(2017-2-24) 101
13.Collection 和 Map 的集成体系(2017-11-14-lyq) 102
14.Map 中的 key 和 value 可以为 null 么?(2017-11-21-gxb) 103
九、 Java 的多线程和并发库 104
(一)多线程基础知识–传统线程机制的回顾(2017-12-11-wl) 104
(二)多线程基础知识–线程并发库(2017-12-11-wl) 118
(三)多线程面试题 246
十、 Java 内部类 272
1.静态嵌套类 (Static Nested Class) 和内部类(Inner Class)的不同?(2017-11-16-wl) 272
2.下面的代码哪些地方会产生编译错误?(2017-11-16-wl) 272
第三章 JavaSE 高级 273
一、 Java 中的反射 273 - 说说你对 Java 中反射的理解 273
二、 Java 中的动态代理 273 - 写一个 ArrayList 的动态代理类(笔试题) 273
- 动静态代理的区别,什么场景使用?(2015-11-25) 274
三、 Java 中的设计模式&回收机制 274
1.你所知道的设计模式有哪些 274
2.单例设计模式 275
3.工厂设计模式 276
4.建造者模式(Builder) 279
5.适配器设计模式 280
6.装饰模式(Decorator) 282
7.策略模式(strategy) 283
8.观察者模式(Observer) 285
9.JVM 垃圾回收机制和常见算法 287
10.谈谈 JVM 的内存结构和内存分配 291
11.Java 中引用类型都有哪些?(重要) 293
12.heap 和 stack 有什么区别(2017-2-23) 295
13.解释内存中的栈 (stack) 、堆 (heap) 和方法区 (method area) 的用法(2017-11-12-wl) 302
四、 Java 的类加载器(2015-12-2) 302
1.Java 的类加载器的种类都有哪些? 302
2.类什么时候被初始化? 303
3.Java 类加载体系之 ClassLoader 双亲委托机制 (2017-2-24) 303 - 描述一下 JVM 加载 class (2017-11-15-wl) 307
- 获得一个类对象有哪些方式?(2017-11-23-wzz) 308
五、 JVM 基础知识(2017-11-16-wl) 309
1.既然有 GC 机制,为什么还会有内存泄露的情况 (2017-11-16-wl) 309
六、 GC 基础知识(2017-11-16-wl) 310 - Java 中为什么会有 GC 机制呢?(2017-11-16-wl) 310
2.对于 Java 的 GC 哪些内存需要回收(2017-11-16-wl) 310
3.Java 的 GC 什么时候回收垃圾(2017-11-16-wl) 311
七、 Java8 的新特性以及使用(2017-12-02-wl) 312
1.通过 10 个示例来初步认识 Java8 中的 lambda 表达式(2017-12-02-wl) 312
2.Java8 中的 lambda 表达式要点(2017-12-02-wl) 320
3.Java8 中的 Optional 类的解析(2017-12-02-wl) 322
八、 在开发中遇到过内存溢出么?原因有哪些?解决方法有哪些?(2017-11-23-gxb) 329
第四章 JavaWEB 基础 330
一、JDBC 技术 330
1.说下原生 jdbc 操作数据库流程?(2017-11-25-wzz) 330
2.什么要使用 PreparedStatement?(2017-11-25-wzz) 331
3.关系数据库中连接池的机制是什么?(2017-12-6-lyq) 332
三、Http 协议 333 - http 的长连接和短连接(2017-11-14-lyq) 333
- HTTP/1.1 与 HTTP/1.0 的区别(2017-11-21-wzy) 333
- http 常见的状态码有哪些?(2017-11-23-wzz) 336
- GET 和 POST 的区别?(2017-11-23-wzz) 337
- http 中重定向和请求转发的区别?(2017-11-23-wzz) 338
四、Cookie 和 Session 338
1.Cookie 和 Session 的区别(2017-11-15-lyq) 338
2.session 共享怎么做的(分布式如何实现 session 共享)? 339
3.在单点登录中,如果 cookie 被禁用了怎么办?(2017-11-23-gxb) 342
五、jsp 技术 342
1.什么是 jsp,什么是 Servlet?jsp 和 Servlet 有什么区别?(2017-11-23-wzz) 342
2.jsp 有哪些域对象和内置对象及他们的作用?(2017-11-25-wzz) 343
六、XML 技术 344 - 什么是 xml,使用 xml 的优缺点,xml 的解析器有哪几种,分别有什么区别?(2017-11-25-wzz) 344
第五章 JavaWEB 高级 346
一 、 Filter 和 Listener 346
二、AJAX 346 - 谈谈你对 ajax 的认识?(2017-11-23-wzz) 346
- jsonp 原 理(2017-11-21-gxb) 347
三、Linux 348 - 说一下常用的 Linux 命令 348
- Linux 中如何查看日志?(2017-11-21-gxb) 349
- Linux 怎么关闭进程(2017-11-21-gxb) 350
四、 常见的前端框架有哪些 351 - EasyUI(2017-11-23-lyq) 351
- MiniUI(2017-11-23-lyq) 353
- jQueryUI(2017-11-23-lyq) 354
- Vue.js(2017-11-23-lyq) 355
- AngularJS (2017-11-23-lyq) 357
第六章 数据库 361
一、 Mysql 361 - SQL 的 select 语句完整的执行顺序(2017-11-15-lyq) 361
- SQL 之聚合函数(2017-11-15-lyq) 363
- SQL 之连接查询(左连接和右连接的区别)(2017-11-15-lyq) 363
- SQL 之 sql 注入(2017-11-15-lyq) 364
- Mysql 性能优化(2017-11-15-lyq) 364
- 必看 sql 面试题(学生表_课程表_成绩表_教师表)(2017-11-25-wzz) 365
- Mysql 数据库架构图(2017-11-25-wzz) 366
- Mysql 架构器中各个模块都是什么?(2017-11-25-wzz) 367
- Mysql 存储引擎有哪些?(2017-11-25-wzz) 368
- MySQL 事务介绍(2017-11-25-wzz) 369
- MySQL 怎么创建存储过程(2017-11-25-wzz) 371
- MySQL 触发器怎么写?(2017-11-25-wzz) 372
- MySQL 语句优化(2017-11-26-wzz) 373
14.MySQL 中文乱码问题完美解决方案(2017-12-07-lwl) 374
15.如何提高 MySQL 的安全性(2017-12-8-lwl) 376
二、 Oracle 378 - 什么是存储过程,使用存储过程的好处?(2017-11-25-wzz) 378
- Oracle 存储过程怎么创建?(2017-11-25-wzz) 379
- 如何使用 Oracle 的游标?(2017-11-25-wzz) 380
4.Oracle 中字符串用什么连接?(2017-11-25-wzz) 380
5.Oracle 中是如何进行分页查询的?(2017-11-25-wzz) 381
6.存储过程和存储函数的特点和区别?(2017-11-25-wzz) 381 - 存储过程与 SQL 的对比?(2017-11-21-gxb) 381
- 你觉得存储过程和 SQL 语句该使用哪个?(2017-11-21-gxb) 382
- 触发器的作用有哪些?(2017-11-21-gxb) 383
- 在千万级的数据库查询中,如何提高效率?(2017-11-23-gxb) 383
第七章 框架 387
一、 SpringMVC 387
1.SpringMVC 的工作原理(2017-11-13-lyq) 387
2.SpringMVC 常用注解都有哪些?(2017-11-24-gxb) 388 - 如何开启注解处理器和适配器?(2017-11-24-gxb) 388
- 如何解决 get 和 post 乱码问题?(2017-11-24-gxb) 388
二、 Spring 389 - 谈谈你对 Spring 的理解(2017-11-13-lyq) 389
- Spring 中的设计模式(2017-11-13-lyq) 389
- Spring 的常用注解(2017-11-13-lyq) 390
- 简单介绍一下 Spring bean 的生命周期(2017-11-21-gxb) 391
- Spring 结构图(2017-11-22-lyq) 392
- Spring 能帮我们做什么?(2017-11-22-lyq) 394
7.请描述一下 Spring 的事务(2017-11-22-lyq) 395
8.BeanFactory 常用的实现类有哪些?(2017-12-03-gxb) 398
9.解释 Spring JDBC、Spring DAO 和 Spring ORM(2017-12-03-gxb) 399
10.简单介绍一下 Spring WEB 模块。(2017-12-03-gxb) 399
11.Spring 配置文件有什么作用?(2017-12-03-gxb) 400 - 什么是 Spring IOC 容器?(2017-12-03-gxb) 400
13.IOC 的优点是什么? 400
14.ApplicationContext 的实现类有哪些? (2017-12-03-gxb) 400
15.BeanFactory 与 AppliacationContext 有什么区别(2017-12-03-gxb) 401 - 什么是 Spring 的依赖注入?(2017-12-04-gxb) 401
- 有哪些不同类型的 IOC(依赖注入)方式?(2017-12-04-gxb) 401
- 什么是 Spring beans? (2017-12-04-gxb) 402
- 一个 Spring Beans 的定义需要包含什么?(2017-12-04-gxb) 402
- 你怎样定义类的作用域? (2017-12-04-gxb) 403
21.Spring 支持的几种 bean 的作用域。(2017-12-04-gxb) 403
22.Spring 框架中的单例 bean 是线程安全的吗? (2017-12-04-gxb) 403
23.什么是 Spring 的内部 bean?(2017-12-04-gxb) 404
24.在 Spring 中如何注入一个 java 集合?(2017-12-04-gxb) 404 - 什么是 bean 的自动装配?(2017-12-04-gxb) 404
- 解释不同方式的自动装配 。(2017-12-04-gxb) 404
- 什么是基于 Java 的 Spring 注解配置? 给一些注解的例子(2017-12-05-gxb) 405
- 什么是基于注解的容器配置? (2017-12-05-gxb) 405
- 怎样开启注解装配?(2017-12-05-gxb) 405
30.在 Spring 框架中如何更有效地使用 JDBC? (2017-12-05-gxb) 405
31.使用 Spring 通过什么方式访问 Hibernate? (2017-12-05-gxb) 406
32.Spring 支持的 ORM 框架有哪些?(2017-12-05-gxb) 406
33.简单解释一下 spring 的 AOP(2017-12-05-gxb) 406
34.在 Spring AOP 中,关注点和横切关注的区别是什么?(2017-12-05-gxb) 407 - 什么是连接点?(2017-12-05-gxb) 407
- Spring 的通知是什么?有哪几种类型?(2017-12-05-gxb) 407
- 什么是切点?(2017-12-05-gxb) 408
- 什么是目标对象? (2017-12-05-gxb) 408
- 什么是代理? (2017-12-05-gxb) 408
- 什么是织入?什么是织入应用的不同点?(2017-12-05-gxb) 408
三、 Shiro 408 - 简单介绍一下 Shiro 框架(2017-11-23-gxb) 408
- Shiro 主要的四个组件(2017-12-2-wzz) 409
- Shiro 运行原理(2017-12-2-wzz) 410
- Shiro 的四种权限控制方式(2017-12-2-wzz) 411
- 授权实现的流程(2017-12-2-wzz) 411
四、 Mybatis 412 - Mybatis 中#和$的区别?(2017-11-23-gxb) 412
2.Mybatis 的编程步骤是什么样的?(2017-12-2-wzz) 413
3.JDBC 编程有哪些不足之处,MyBatis 是如何解决这些问题的?(2017-12-2-wzz) 413
4.使用 MyBatis 的 mapper 接口调用时有哪些要求?(2017-12-2-wzz) 414
5.Mybatis 中一级缓存与二级缓存?(2017-12-4-lyq) 414
6.MyBatis 在 insert 插入操作时返回主键 ID(2017-12-4-lyq) 415
五、 Struts2 415 - 简单介绍一下 Struts2(2017-11-24-gxb) 415
- Struts2 的执行流程了解么?(2017-11-24-gxb) 416
- Struts2 中 Action 配置的注意事项有哪些?(2017-11-24-gxb) 418
- 拦截器和过滤器有哪些区别?(2017-11-24-gxb) 419
- Struts2 的封装方式有哪些?(2017-11-24-gxb) 419
6.简单介绍一下 Struts2 的值栈。(2017-11-24-gxb) 421
7.SpringMVC 和 Struts2 的区别?(2017-11-23-gxb) 422 - Struts2 中的 # 和 % 分别是做什么的?(2017-11-30-wzz) 423
- Struts2 中有哪些常用结果类型?(2017-12-1-lyq) 424
六、 Hibernate 424
1.简述一下 hibernate 的开发流程(2017-11-24-gxb) 424
2.hibernate 中对象的三种状态(2017-11-24-gxb) 425
3.hibernate 的缓存机制。(2017-11-24-gxb) 425
4.Hibernate 的查询方式有哪些?(2017-11-24-gxb) 426
5.Hibernate 和 Mybatis 的区别?(2017-11-23-gxb) 427
6.Hibernate 和 JDBC 优缺点对比(2017-11-29-wzz) 427
7.关于 Hibernate 的 orm 思想你了解多少?(2017-11-29-wzz) 428 - get 和 load 的区别?(2017-11-30-wzz) 429
9.如何进行 Hibernate 的优化?(2017-11-30-wzz) 429
10.什么是 Hibernate 延迟加载?(2017-12-1-lyq) 430
11.No Session 问题原理及解决方法?(2017-12-4-lyq) 430
12.Spring 的两种代理 JDK 和 CGLIB 的区别浅谈(2017-12-4-lyq) 432
13.叙述 Session 的缓存的作用(2017-12-9-lwl) 432
14.Session 的清理和清空有什么区别?(2017-12-10-lwl) 433
15.请简述 Session 的特点有哪些?(2017-12-10-lwl) 433
16.比较 Hibernate 三种检索策略的优缺点(2017-12-10-lwl) 433
七、 Quartz 定时任务 434 - 什么是 Quartz 框架(2017-12-2-wzz) 434
2.配置文件 applicationContext_job.xml 各个属性作用(2017-12-2-wzz) 434
3.Cron 表达式详解(2017-12-2-wzz) 435 - 如何监控 Quartz 的 job 执行状态:运行中,暂停中,等待中? (2017-12-2-wzz) 435
第八章 最新技术 436
一、 Redis 436 - Redis 的特点?(2017-11-25-wzz) 436
2.为什么 redis 需要把所有数据放到内存中?(2017-11-25-wzz) 436
3.Redis 常见的性能问题都有哪些?如何解决?(2017-11-25-wzz) 437 - Redis 最适合的场景有哪些?(2017-11-25-wzz) 437
5.Memcache 与 Redis 的区别都有哪些?(2017-11-25-wzz) 437
6.Redis 用过 RedisNX 吗?Redis 有哪几种数据结构?(2017-11-14-lyq) 438 - Redis 的优缺点(2017-11-22-lyq) 439
- Redis 的持久化(2017-11-23-lyq) 440
二、 消息队列 ActiveMQ 442 - 如何使用 ActiveMQ 解决分布式事务?(2017-11-21-gxb) 442
- 了解哪些消息队列?(2017-11-24-gxb) 443
- ActiveMQ 如果消息发送失败怎么办?(2017-11-24-gxb) 444
三、 Dubbo 445 - Dubbo 的容错机制有哪些。(2017-11-23-gxb) 445
- 使用 dubbo 遇到过哪些问题?(2017-11-23-gxb) 446
- Dubbo 的连接方式有哪些?(2017-12-1-lyq) 447
四、 并发相关 450 - 如何测试并发量?(2017-11-23-gxb) 450
五、 Nginx 451
1.Nginx 反向代理为什么能够提升服务器性能?(2017-11-24-gxb) 451
2.Nginx 和 Apache 各有什么优缺点? (2017-11-24-gxb) 451
3.Nginx 多进程模型是如何实现高并发的?(2017-12-5-lyq) 452
六、 Zookeeper 453
1.简单介绍一下 zookeeper 以及 zookeeper 的原理。(2017-11-24-gxb) 453
七、 solr 454 - 简单介绍一下 solr(2017-11-24-gxb) 454
2.solr 怎么设置搜索结果排名靠前?(2017-11-24-gxb) 454
3.solr 中 IK 分词器原理是什么?(2017-11-24-gxb) 455
八、 webService 455 - 什么是 webService?(2017-11-24-lyq) 455
- 常见的远程调用技术(2017-11-24-lyq) 455
九、 Restful 456 - 谈谈你对 restful 的理解以及在项目中的使用?(2017-11-30-wzz) 456
第九章 企业实战面试题 457
一、 智慧星(2017-11-25-wmm) 457 - 选 择题 457
- 编 程题 460
二、 中讯志远科技(2017-11-26-wmm) 463 - 问 答题 463
三、 腾讯(2016 年校招面试题 2017-11-29-wzy) 467 - 选 择题 467
四、 北京宝蓝德股份科技有限公司(2017-12-03-wmm) 481
1.选择题 481
2.问答题 483
五、 智慧流(2017-12-04-wmm) 485
1.选择题 485 - 问 答题 490
- 逻辑思维题 492
六、 某公司(2017-12-05-wmm) 495 - 选 择题 495
- 问 答题 505
七、 华胜天成(2017-12-11-wzy) 521 - 不定项选择题 521
- 简 答题 532
八、 诚迈(2017-12-7-lyq) 532 - 选 择题 532
- 判 断题 534
- 简 答题 534
- 编 程题 538
5.linux 试题 542
6.数据库试题 545
7.应用服务器试题 545
九、 科大讯飞(2017-12-11-lyq) 547
十、 泰瑞(2017-12-16-wmm) 552 - 笔 试题 552
- 上 机题 553
十一、 文思创新(2017-12-17-wmm) 556
1.什么叫对象?什么叫类?什么面向对象(OOP)? 556
2.相对于 JDK1.4,JDK1.5 有哪些新特性? 557
3.JAVA 中使用 final 修饰符,对程序有哪些影响? 557
4.Java 环境变量 Unix/Linux 下如何配置? 558
5.写出 5 个你在 JAVA 开发中常用的包含(全名),并简述其作用。 559
6.写出 5 个常见的运行时异常(RuntimeException)。 560
7.方法重载(overload)需要满足什么条件,方法覆盖/方法重写(override)需要满足什么条件?(二选一)
… 560
8.继承(inheritance)的优缺点是什么? 561
9.为什么要使用接口和抽象类? 562
10.什么是自定义异常?如何自定义异常? 563
11.Set,List,Map 有什么区别? 563
12.什么叫对象持久化(OBJect PERSIstence),为什么要进行对象持久化? 564
13.JavaScript 有哪些优缺点? 564
14.Jsp 有什么特点? 565
15.什么叫脏数据,什么叫脏读(Dirty Read) 566
第十章 项目业务逻辑问题 566
一、 传统项目(2017-12-5-lyq) 566
1.什么是 BOS? 566
2.Activity 工 作流 567
第一章 内容介绍
该宝典是一份知识点全面又能不断更新,与时俱进的学习手册,不仅收录了作者亲身面试遇到的问题,还收录了近上万名黑马学子面试时遇到的问题。我们会一直不断地更新和充实该宝典,同时也希望读者朋友能够多多提供优质的面试题,也许下一个版本就有你提供的面试题哦。
本人的面试实战记录发布在黑马论坛:http://bbs.itheima.com/thread-196394-1-1.html
大家可以访问上面的网址,通过阳哥的实战记录略微感知一下真实面试的情况,从中学习一些面试技巧以便让自己在未来的面试中能够得心应手,顺利拿到自己喜欢的 offer。
注意:该面试宝典仅供参考,由于作者本人的知识水平有限加之编写时间仓促因此难免有 bug 的存在,希望大
家见谅。
该宝典的一个明确目标是能够让 90%以上的 Java 技术面试题都落到该宝典中,如果您有不错的知识或者面试题, 您可以发送到 wangzhenyang@itcast.cn,本人将不胜感激。让天下没有难学的知识,希望你我的努力能帮到更多的莘莘学子。
世间事,很多都可投机取巧,但技术却必须靠日积月累的努力来提高。本宝典更加注重的是知识的掌握,而不仅仅是对面试题的应付。在展示常见的面试问题以及回答技巧的同时还详细讲解了每一道题所包含的知识点,让读者不仅知其然,更知其所以然。
第二章 JavaSE 基础
一、Java 面向对象
1.面向对象都有哪些特性以及你对这些特性的理解
1)继承:继承是从已有类得到继承信息创建新类的过程。提供继承信息的类被称为父类(超类、基类);得到继承信息的类被称为子类(派生类)。继承让变化中的软件系统有了一定的延续性,同时继承也是封装程序中可变因素的重要手段。
2)封装:通常认为封装是把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口。面向对象的本质就是将现实世界描绘成一系列完全自治、封闭的对象。我们在类中编写的方法就是对实现细节的一种封装;我们编写一个类就是对数据和数据操作的封装。可以说,封装就是隐藏一切可隐藏的东西,只向外界提供最简单的编程接口。
3)多态性:多态性是指允许不同子类型的对象对同一消息作出不同的响应。简单的说就是用同样的对象引用调用同样的方法但是做了不同的事情。多态性分为编译时的多态性和运行时的多态性。如果将对象的方法视为对象向外界提供的服务,那么运行时的多态性可以解释为:当 A 系统访问 B 系统提供的服务时,B 系统有多种提供服务的方式, 但一切对 A 系统来说都是透明的。方法重载(overload)实现的是编译时的多态性(也称为前绑定),而方法重写
(override)实现的是运行时的多态性(也称为后绑定)。运行时的多态是面向对象最精髓的东西,要实现多态需要做两件事:1. 方法重写(子类继承父类并重写父类中已有的或抽象的方法);2. 对象造型(用父类型引用引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为)。
4)抽象:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。
注意:默认情况下面向对象有 3 大特性,封装、继承、多态,如果面试官问让说出 4 大特性,那么我们就把抽象加上去。
2.访问权限修饰符 public、private、protected, 以及不写(默认)时的区别(2017- 11-12)
该题目比较简单,不同的权限修饰符的区别见下表。
修饰符 当前类 同 包 子 类 其他包
public √ √ √ √
protected √ √ √ ×
default √ √ × ×
private √ × × ×
3.如何理解 clone 对象
3.1为什么要用 clone?
在实际编程过程中,我们常常要遇到这种情况:有一个对象 A,在某一时刻 A 中已经包含了一些有效值,此时可能会需要一个和 A 完全相同新对象 B,并且此后对 B 任何改动都不会影响到 A 中的值,也就是说,A 与 B 是两个独立的对象,但 B 的初始值是由 A 对象确定的。在 Java 语言中,用简单的赋值语句是不能满足这种需求的。要满足这种需求虽然有很多途径,但实现 clone()方法是其中最简单,也是最高效的手段。
3.2new 一个对象的过程和 clone 一个对象的过程区别
new 操作符的本意是分配内存。程序执行到 new 操作符时,首先去看 new 操作符后面的类型,因为知道了类型, 才能知道要分配多大的内存空间。分配完内存之后,再调用构造函数,填充对象的各个域,这一步叫做对象的初始化,
22
构造方法返回后,一个对象创建完毕,可以把他的引用(地址)发布到外部,在外部就可以使用这个引用操纵这个对象。
clone 在第一步是和 new 相似的,都是分配内存,调用 clone 方法时,分配的内存和原对象(即调用 clone 方法的对象)相同,然后再使用原对象中对应的各个域,填充新对象的域,填充完成之后,clone 方法返回,一个新的相同的对象被创建,同样可以把这个新对象的引用发布到外部。
3.3clone 对象的使用
3.3.1复制对象和复制引用的区别
1.Person p = new Person(23, “zhang”);
2.Person p1 = p;
3.System.out.println§;
4.System.out.println(p1);
当 Person p1 = p;执行之后, 是创建了一个新的对象吗? 首先看打印结果:
1.com.itheima.Person@2f9ee1ac
2.com.itheima.Person@2f9ee1ac
可以看出,打印的地址值是相同的,既然地址都是相同的,那么肯定是同一个对象。p 和 p1 只是引用而已,他们都指向了一个相同的对象 Person(23, “zhang”) 。 可以把这种现象叫做引用的复制。上面代码执行完成之后, 内存中的情景如下图所示:
而下面的代码是真真正正的克隆了一个对象。
1.Person p = new Person(23, “zhang”);
23
2.Person p1 = (Person) p.clone(); 3.System.out.println§;
4.System.out.println(p1);
从打印结果可以看出,两个对象的地址是不同的,也就是说创建了新的对象, 而不是把原对象的地址赋给了一个新的引用变量:
1.com.itheima.Person@2f9ee1ac
2.com.itheima.Person@67f1fba0
以上代码执行完成后, 内存中的情景如下图所示:
3.3.2深拷贝和浅拷贝
上面的示例代码中,Person 中有两个成员变量,分别是 name 和 age, name 是 String 类型, age 是 int 类型。代码非常简单,如下所示:
13. return name;
14. }
15. @Override
16. protected Object clone() throws CloneNotSupportedException { 17. return (Person)super.clone();
18. }
19.}
由于 age 是基本数据类型,那么对它的拷贝没有什么疑议,直接将一个 4 字节的整数值拷贝过来就行。但是 name 是 String 类型的, 它只是一个引用, 指向一个真正的 String 对象,那么对它的拷贝有两种方式: 直接将原对象中的 name 的引用值拷贝给新对象的 name 字段, 或者是根据原 Person 对象中的 name 指向的字符串对象创建一个新的相同的字符串对象,将这个新字符串对象的引用赋给新拷贝的 Person 对象的 name 字段。这两种拷贝方式分别叫做浅拷贝和深拷贝。深拷贝和浅拷贝的原理如下图所示:
下面通过代码进行验证。如果两个 Person 对象的 name 的地址值相同, 说明两个对象的 name 都指向同一个
String 对象,也就是浅拷贝, 而如果两个对象的 name 的地址值不同, 那么就说明指向不同的 String 对象, 也就是在拷贝 Person 对象的时候, 同时拷贝了 name 引用的 String 对象, 也就是深拷贝。验证代码如下:
1.Person p = new Person(23, “zhang”);
2.Person p1 = (Person) p.clone();
3.String result = p.getName() == p1.getName()
4.? “clone 是浅拷贝的” : “clone 是深拷贝的”;
5.System.out.println(result);
打印结果为:
6. clone 是浅拷贝的
所以,clone 方法执行的是浅拷贝, 在编写程序时要注意这个细节。
如何进行深拷贝:
由上一节的内容可以得出如下结论:如果想要深拷贝一个对象,这个对象必须要实现 Cloneable 接口,实现 clone 方法,并且在 clone 方法内部,把该对象引用的其他对象也要 clone 一份,这就要求这个被引用的对象必须也要实现Cloneable 接口并且实现 clone 方法。那么,按照上面的结论,实现以下代码 Body 类组合了 Head 类,要想深拷贝
Body 类,必须在 Body 类的 clone 方法中将 Head 类也要拷贝一份。代码如下:
1.static class Body implements Cloneable{
2. public Head head;
3. public Body() {}
4. public Body(Head head) {this.head = head;} 5. @Override
6. protected Object clone() throws CloneNotSupportedException { 7. Body newBody = (Body) super.clone();
8. newBody.head = (Head) head.clone(); 9. return newBody;
10. }
11.}
12.static class Head implements Cloneable{ 13. public Face face;
14. public Head() {} 15. @Override
16. protected Object clone() throws CloneNotSupportedException { 17. return super.clone();
18. } }
19.public static void main(String[] args) throws CloneNotSupportedException { 20. Body body = new Body(new Head(new Face()));
21. Body body1 = (Body) body.clone();
22. System.out.println("body == body1 : " + (body == body1) );
23. System.out.println("body.head == body1.head : " + (body.head == body1.head));
24.}
打印结果为:
1.body == body1 : false
2.body.head == body1.head : false
二、JavaSE 语法(2017-11-12-wl)
1.Java 有没有 goto 语句?(2017-11-12-wl)
goto 是 Java 中的保留字,在目前版本的 Java 中没有使用。根据 James Gosling(Java 之父)编写的《The Java Programming Language》一书的附录中给出了一个 Java 关键字列表,其中有 goto 和 const,但是这两个是目前无法使用的关键字,因此有些地方将其称之为保留字,其实保留字这个词应该有更广泛的意义,因为熟悉 C 语言的程序员都知道,在系统类库中使用过的有特殊意义的单词或单词的组合都被视为保留字。
2. & 和 && 的区别(2017-11-12-wl)
&运算符有两种用法:(1)按位与;(2)逻辑与。
&&运算符是短路与运算。逻辑与跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端的布尔值都是
true 整个表达式的值才是 true。
&&之所以称为短路运算是因为,如果&&左边的表达式的值是 false,右边的表达式会被直接短路掉,不会进行运算。很多时候我们可能都需要用&&而不是&,例如在验证用户登录时判定用户名不是 null 而且不是空字符串,应当写为 username != null &&!username.equals(""),二者的顺序不能交换,更不能用&运算符,因为第一个条件如果不成立,根本不能进行字符串的 equals 比较,否则会产生 NullPointerException 异常。注意:逻辑或运算符(|) 和短路或运算符(||)的差别也是如此。
3.在 Java 中,如何跳出当前的多重嵌套循环(2017-11-14-wl)
在最外层循环前加一个标记如 A,然后用 break A;可以跳出多重循环。(Java 中支持带标签的 break 和 continue语句,作用有点类似于 C 和 C++中的 goto 语句,但是就像要避免使用 goto 一样,应该避免使用带标签的 break 和 continue,因为它不会让你的程序变得更优雅,很多时候甚至有相反的作用)。
27
4.两个对象值相同 (x.equals(y) == true) ,但却可有不同的 hashCode,这句
话对不对?(2017-11-14-wl)
不对,如果两个对象 x 和 y 满足 x.equals(y) == true,它们的哈希码(hashCode)应当相同。
Java 对于 eqauls 方法和 hashCode 方法是这样规定的:(1)如果两个对象相同(equals 方法返回 true),那么它们的 hashCode 值一定要相同;(2)如果两个对象的 hashCode 相同,它们并不一定相同。当然,你未必要按照要求去做,但是如果你违背了上述原则就会发现在使用容器时,相同的对象可以出现在 Set 集合中,同时增加新元素的效率会大大下降(对于使用哈希存储的系统,如果哈希码频繁的冲突将会造成存取性能急剧下降)。
关于 equals 和 hashCode 方法,很多 Java 程序员都知道,但很多人也就是仅仅知道而已,在 Joshua Bloch 的大作《Effective Java》(很多软件公司,《Effective Java》、《Java 编程思想》以及《重构:改善既有代码质量》是 Java 程序员必看书籍,如果你还没看过,那就赶紧去买一本吧)中是这样介绍 equals 方法的。
首先 equals 方法必须满足自反性(x.equals(x)必须返回 true)、对称性(x.equals(y)返回 true 时,y.equals(x) 也必须返回 true)、传递性(x.equals(y)和 y.equals(z)都返回 true 时,x.equals(z)也必须返回 true)和一致性(当
x 和 y 引用的对象信息没有被修改时,多次调用 x.equals(y)应该得到同样的返回值),而且对于任何非 null 值的引用 x,x.equals(null)必须返回 false。实现高质量的 equals 方法的诀窍包括:1. 使用==操作符检查"参数是否为这个对象的引用";2. 使用 instanceof 操作符检查"参数是否为正确的类型";3. 对于类中的关键属性,检查参数传入对象的属性是否与之相匹配;4. 编写完 equals 方法后,问自己它是否满足对称性、传递性、一致性;5. 重写 equals 时总是要重写 hashCode;6. 不要将 equals 方法参数中的 Object 对象替换为其他的类型,在重写时不要忘掉
@Override 注解。
5.是否可以继承 String (2017-11-14-wl)
String 类是 final 类,不可以被继承。
28
继承 String 本身就是一个错误的行为,对 String 类型最好的重用方式是关联关系(Has-A)和依赖关系(Use-
A)而不是继承关系(Is-A)。
6.当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递?(2017-11-14-wl)
是值传递。Java 语言的方法调用只支持参数的值传递。当一个对象实例作为一个参数被传递到方法中时,参数的值就是对该对象的引用。对象的属性可以在被调用过程中被改变,但对对象引用的改变是不会影响到调用者的。C++ 和 C#中可以通过传引用或传输出参数来改变传入的参数的值。说明:Java 中没有传引用实在是非常的不方便,这一点在 Java 8 中仍然没有得到改进,正是如此在 Java 编写的代码中才会出现大量的 Wrapper 类(将需要通过方法调用修改的引用置于一个 Wrapper 类中,再将 Wrapper 对象传入方法),这样的做法只会让代码变得臃肿,尤其是让从 C 和 C++转型为 Java 程序员的开发者无法容忍。
7.重载(overload)和重写(override)的区别?重载的方法能否根据返回类型进行区分?(2017-11-15-wl)
方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。重载对返回类型没有特殊的要求。
方法重载的规则:
1.方法名一致,参数列表中参数的顺序,类型,个数不同。
2.重载与方法的返回值无关,存在于父类和子类,同类中。
3.可以抛出不同的异常,可以有不同修饰符。
29
方法重写的规则:
1.参数列表必须完全与被重写方法的一致,返回类型必须完全与被重写方法的返回类型一致。
2.构造方法不能被重写,声明为 final 的方法不能被重写,声明为 static 的方法不能被重写,但是能够被再次声明。
3.访问权限不能比父类中被重写的方法的访问权限更低。
4.重写的方法能够抛出任何非强制异常(UncheckedException,也叫非运行时异常),无论被重写的方法是否抛出异常。但是,重写的方法不能抛出新的强制性异常,或者比被重写方法声明的更广泛的强制性异常,反之则可以。
8.为什么函数不能根据返回类型来区分重载?(2017-11-15-wl)
该道题来自华为面试题。
因为调用时不能指定类型信息,编译器不知道你要调用哪个函数。例如:
当调用 max(1, 2);时无法确定调用的是哪个,单从这一点上来说,仅返回值类型不同的重载是不应该允许的。再比如对下面这两个方法来说,虽然它们有同样的名字和自变量,但其实是很容易区分的:
若编译器可根据上下文(语境)明确判断出含义,比如在 int x=f()中,那么这样做完全没有问题。然而, 我们也可能调用一个方法,同时忽略返回值;我们通常把这称为“为它的副作用去调用一个方法”,因为我
们关心的不是返回值,而是方法调用的其他效果。所以假如我们像下面这样调用方法: f(); Java 怎样判断 f()的具体调用方式呢?而且别人如何识别并理解代码呢?由于存在这一类的问题,所以不能。
30
函数的返回值只是作为函数运行之后的一个“状态”,他是保持方法的调用者与被调用者进行通信的关键。并不能作为某个方法的“标识”。
9.char 型变量中能不能存储一个中文汉字,为什么?(2017-11-16-wl)
char 类型可以存储一个中文汉字,因为 Java 中使用的编码是 Unicode(不选择任何特定的编码,直接
使用字符在字符集中的编号,这是统一的唯一方法),一个 char 类型占 2 个字节(16 比特),所以放一个中文是没问题的。
补充:使用 Unicode 意味着字符在 JVM 内部和外部有不同的表现形式,在 JVM 内部都是 Unicode,当这个字符被从 JVM 内部转移到外部时(例如存入文件系统中),需要进行编码转换。所以 Java 中有字节流和字符流,以及在字符流和字节流之间进行转换的转换流,如 InputStreamReader 和 OutputStreamReader,这两个类是字节流和字符流之间的适配器类,承担了编码转换的任务;对于 C 程序员来说,要完成这样的编码转换恐怕要依赖于 union(联合体/共用体)共享内存的特征来实现了。
10.抽象类(abstract class)和接口(interface)有什么异同?(2017-11-16-wl)
不同:
抽象类:
1.抽象类中可以定义构造器
2.可以有抽象方法和具体方法
3.接口中的成员全都是 public 的
4.抽象类中可以定义成员变量
5.有抽象方法的类必须被声明为抽象类,而抽象类未必要有抽象方法
6.抽象类中可以包含静态方法
31
7.一个类只能继承一个抽象类接口:
1.接口中不能定义构造器
2.方法全部都是抽象方法
3.抽象类中的成员可以是 private、默认、protected、public
4.接口中定义的成员变量实际上都是常量
5.接口中不能有静态方法
6.一个类可以实现多个接口
相同:
1.不能够实例化
2.可以将抽象类和接口类型作为引用类型
3.一个类如果继承了某个抽象类或者实现了某个接口都需要对其中的抽象方法全部进行实现,否则该类仍然需要被声明为抽象类
11.抽象的(abstract)方法是否可同时是静态的(static), 是否可同时是本地方法
(native),是否可同时被 synchronized(2017-11-16-wl)
都不能。抽象方法需要子类重写,而静态的方法是无法被重写的,因此二者是矛盾的。本地方法是由
本地代码(如 C 代码)实现的方法,而抽象方法是没有实现的,也是矛盾的。synchronized 和方法的实现细节有关, 抽象方法不涉及实现细节,因此也是相互矛盾的。
12.阐述静态变量和实例变量的区别?(2017-11-16-wl)
32
管创建多少个对象,静态变量在内存中有且仅有一个拷贝;
实例变量: 必须依存于某一实例,需要先创建对象然后通过对象才能访问到它。静态变量可以实现让多个对象共享内存。
13. ==和 equals 的区别?(2017-11-22-wzz)
equals 和== 最大的区别是一个是方法一个是运算符。
==:如果比较的对象是基本数据类型,则比较的是数值是否相等;如果比较的是引用数据类型,则比较的是对象的地址值是否相等。
equals():用来比较方法两个对象的内容是否相等。
注意:equals 方法不能用于基本数据类型的变量,如果没有对 equals 方法进行重写,则比较的是引用类型的变量所指向的对象的地址。
14.break 和 continue 的区别?(2017-11-23-wzz)
break 和 continue 都是用来控制循环的语句。
break 用于完全结束一个循环,跳出循环体执行循环后面的语句。
continue 用于跳过本次循环,执行下次循环。
15.String s = “Hello”;s = s + " world!";这两行代码执行后,原始的 String 对象中的内容到底变了没有?(2017-12-1-lyq)
没有。因为 String 被设计成不可变(immutable)类,所以它的所有对象都是不可变对象。在这段代码中,s 原先指向一个 String 对象,内容是 “Hello”,然后我们对 s 进行了“+”操作,那么 s 所指向的那个对象是否发生了改变呢? 答案是没有。这时,s 不指向原来那个对象了,而指向了另一个 String 对象,内容为"Hello world!",原来那个对象还
存在于内存之中,只是 s 这个引用变量不再指向它了。
通过上面的说明,我们很容易导出另一个结论,如果经常对字符串进行各种各样的修改,或者说,不可预见的修改,那么使用 String 来代表字符串的话会引起很大的内存开销。因为 String 对象建立之后不能再改变,所以对于每一个不同的字符串,都需要一个 String 对象来表示。这时,应该考虑使用 StringBuffer 类,它允许修改,而不是每个不同的字符串都要生成一个新的对象。并且,这两种类的对象转换十分容易。同时,我们还可以知道,如果要使用内容相同的字符串,不必每次都 new 一个 String。例如我们要在构造器中对一个名叫 s 的 String 引用变量进行初始化, 把它设置为初始值,应当这样做:
而非
1.s = new String(“Initial Value”);
后者每次都会调用构造器,生成新对象,性能低下且内存开销大,并且没有意义,因为 String 对象不可改变,所以对于内容相同的字符串,只要一个 String 对象来表示就可以了。也就说,多次调用上面的构造器创建多个对象,他们的 String 类型属性 s 都指向同一个对象。
上面的结论还基于这样一个事实:对于字符串常量,如果内容相同,Java 认为它们代表同一个 String 对象。而用关键字 new 调用构造器,总是会创建一个新的对象,无论内容是否相同。 至于为什么要把 String 类设计成不可变类,是它的用途决定的。其实不只 String,很多 Java 标准类库中的类都是不可变的。在开发一个系统的时候,我们有时候也需要设计不可变类,来传递一组相关的值,这也是面向对象思想的体现。不可变类有一些优点,比如因为它的对象是只读的,所以多线程并发访问也不会有任何问题。当然也有一些缺点,比如每个不同的状态都要一个对象来代表,可能会造成性能上的问题。所以 Java 标准类库还提供了一个可变版本,即 StringBuffer。
三、Java 中的多态
- Java 中实现多态的机制是什么?
靠的是父类或接口定义的引用变量可以指向子类或具体实现类的实例对象,而程序调用的方法在运行期才动态绑定,就是引用变量所指向的具体实例对象的方法,也就是内存里正在运行的那个对象的方法,而不是引用变量的类型中定义的方法。
四、Java 的异常处理
1.Java 中异常分为哪些种类
1)按照异常需要处理的时机分为编译时异常(也叫强制性异常)也叫 CheckedException 和运行时异常
(也叫非强制性异常)也叫 RuntimeException。只有 java 语言提供了 Checked 异常,Java 认为 Checked 异常都是可以被处理的异常,所以 Java 程序必须显式处理 Checked 异常。如果程序没有处理 Checked 异常,该程序在编译时就会发生错误无法编译。这体现了 Java 的设计哲学:没有完善错误处理的代码根本没有机会被执行。对 Checked 异常处理方法有两种:
1当前方法知道如何处理该异常,则用 try…catch 块来处理该异常。
2当前方法不知道如何处理,则在定义该方法是声明抛出该异常。
运行时异常只有当代码在运行时才发行的异常,编译时不需要 try catch。Runtime 如除数是 0 和数组下标越界等,其产生频繁,处理麻烦,若显示申明或者捕获将会对程序的可读性和运行效率影响很大。所以由系统自动检测并将它们交给缺省的异常处理程序。当然如果你有处理要求也可以显示捕获它们。
2.调用下面的方法,得到的返回值是什么?
1. public int getNum(){
- try {
- int a = 1/0;
4.return 1;
5.} catch (Exception e) {
6.return 2;
7.}finally{
8.return 3; 9. }
代码在走到第 3 行的时候遇到了一个 MathException,这时第四行的代码就不会执行了,代码直接跳转到 catch 语句中,走到第 6 行的时候,异常机制有这么一个原则如果在 catch 中遇到了 return 或者异常等能使该函数终止的话那么有 finally 就必须先执行完 finally 代码块里面的代码然后再返回值。因此代码又跳到第 8 行,可惜第 8 行是一个
return 语句,那么这个时候方法就结束了,因此第 6 行的返回结果就无法被真正返回。如果 finally 仅仅是处理了一个释放资源的操作,那么该道题最终返回的结果就是 2。因此上面返回值是 3。
3.error 和 exception 的区别?(2017-2-23)
Error 类和 Exception 类的父类都是 Throwable 类,他们的区别如下。
Error 类一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢出等。对于这类错误的导致的应用程序中断,仅靠程序本身无法恢复和和预防,遇到这样的错误,建议让程序终止。
Exception 类表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意终止异常。
Exception 类又分为运行时异常(Runtime Exception)和受检查的异常(Checked Exception ),运行时异常;ArithmaticException,IllegalArgumentException,编译能通过,但是一运行就终止了,程序不会处理运行时异常, 出现这类异常,程序会终止。而受检查的异常,要么用 try。。。catch 捕获,要么用 throws 字句声明抛出,交给它的父类处理,否则编译不会通过。
- java 异常处理机制(2017-2-23)
Java 对异常进行了分类,不同类型的异常分别用不同的 Java 类表示,所有异常的根类为 java.lang.Throwable,
Throwable 下面又派生了两个子类:Error 和 Exception,Error 表示应用程序本身无法克服和恢复的一种严重问题。
Exception 表示程序还能够克服和恢复的问题,其中又分为系统异常和普通异常,系统异常是软件本身缺陷所导致的问题,也就是软件开发人员考虑不周所导致的问题,软件使用者无法克服和恢复这种问题,但在这种问题下还可以让软件系统继续运行或者让软件死掉,例如,数组脚本越界(ArrayIndexOutOfBoundsException),空指针异常
(NullPointerException)、类转换异常(ClassCastException);普通异常是运行环境的变化或异常所导致的问题, 是用户能够克服的问题,例如,网络断线,硬盘空间不够,发生这样的异常后,程序不应该死掉。
java 为系统异常和普通异常提供了不同的解决方案,编译器强制普通异常必须 try…catch 处理或用 throws 声明继续抛给上层调用方法处理,所以普通异常也称为 checked 异常,而系统异常可以处理也可以不处理,所以,编译器不强制用 try…catch 处理或用 throws 声明,所以系统异常也称为 unchecked 异常。
5.请写出你最常见的 5 个 RuntimeException(2017-11-22-wzz)
下面列举几个常见的 RuntimeException。
1)java.lang.NullPointerException 空指针异常;出现原因:调用了未经初始化的对象或者是不存在的对象。
2)java.lang.ClassNotFoundException 指定的类找不到;出现原因:类的名称和路径加载错误;通常都是程序试图通过字符串来加载某个类时可能引发异常。
3)java.lang.NumberFormatException 字符串转换为数字异常;出现原因:字符型数据中包含非数字型字符。
4)java.lang.IndexOutOfBoundsException 数组角标越界异常,常见于操作数组对象时发生。
5)java.lang.IllegalArgumentException 方法传递参数错误。
6)java.lang.ClassCastException 数据类型转换异常。
7)java.lang.NoClassDefFoundException 未找到类定义错误。8)SQLException SQL 异常,常见于操作数据库时的 SQL 语句错误。9)java.lang.InstantiationException 实例化异常。
10)java.lang.NoSuchMethodException 方法不存在异常。
6.throw 和 throws 的区别(2017-11-22-wzz)
throw:
1)throw 语句用在方法体内,表示抛出异常,由方法体内的语句处理。
2)throw 是具体向外抛出异常的动作,所以它抛出的是一个异常实例,执行 throw 一定是抛出了某种异常。
throws:
1)throws 语句是用在方法声明后面,表示如果抛出异常,由该方法的调用者来进行异常的处理。
2)throws 主要是声明这个方法会抛出某种类型的异常,让它的使用者要知道需要捕获的异常的类型。
3)throws 表示出现异常的一种可能性,并不一定会发生这种异常。
7.final、finally、finalize 的区别?(2017-11-23-wzz)
1)final:用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,被其修饰的类不可继承。
2)finally:异常处理语句结构的一部分,表示总是执行。
3)finalize:Object 类的一个方法,在垃圾回收器执行的时候会调用被回收对象的此方法,可以覆盖此方法提供垃圾收集时的其他资源回收,例如关闭文件等。该方法更像是一个对象生命周期的临终方法,当该方法被系统调用则代表该对象即将“死亡”,但是需要注意的是,我们主动行为上去调用该方法并不会导致该对 象“死亡”,这是一个被动的方法(其实就是回调方法),不需要我们调用。
五、JavaSE 常用 API
1.Math.round(11.5)等于多少?Math.round(- 11.5) 又等于多少?(2017-11- 14-wl)
Math.round(11.5)的返回值是 12,Math.round(-11.5)的返回值是-11。四舍五入的原理是在参数上加 0.5
然后进行取整。
2.switch 是否能作用在 byte 上,是否能作用在 long 上,是否能作用在 String
上?(2017-11-14-wl)
Java5 以前 switch(expr)中,expr 只能是 byte、short、char、int。从 Java 5 开始,Java 中引入了枚举类型,
expr 也可以是 enum 类型。
从 Java 7 开始,expr 还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的。
3.数组有没有 length() 方法?String 有没有 length() 方法?(2017-11-14-wl)
数组没有 length()方法,而是有 length 的属性。String 有 length()方法。JavaScript 中,获得字符串的长度是通过 length 属性得到的,这一点容易和 Java 混淆。
4.String 、StringBuilder 、StringBuffer 的区别?(2017-11-14-wl)
Java 平台提供了两种类型的字符串:String 和 StringBuffer/StringBuilder,它们都可以储存和操作字符串,区别如下。
1)String 是只读字符串,也就意味着 String 引用的字符串内容是不能被改变的。初学者可能会有这样的误解:
1.String str = “abc”;
2.str = “bcd”;
39
如上,字符串 str 明明是可以改变的呀!其实不然,str 仅仅是一个引用对象,它指向一个字符串对象“abc”。第二行代码的含义是让 str 重新指向了一个新的字符串“bcd”对象,而“abc”对象并没有任何改变,只不过该对象已经成为一个不可及对象罢了。
2)StringBuffer/StringBuilder 表示的字符串对象可以直接进行修改。
3)StringBuilder 是 Java5 中引入的,它和 StringBuffer 的方法完全相同,区别在于它是在单线程环境下使用的,因为它的所有方法都没有被 synchronized 修饰,因此它的效率理论上也比 StringBuffer 要高。
5.什么情况下用“+”运算符进行字符串连接比调用 StringBuffer/StringBuilder
对象的 append 方法连接字符串性能更好?(2017-11-14-wl)
该题来自华为。
字符串是 Java 程序中最常用的数据结构之一。在 Java 中 String 类已经重载了"+"。也就是说,字符串可以直接使用"+“进行连接,如下面代码所示:
1.String s = “abc” + “ddd”;
但这样做真的好吗?当然,这个问题不能简单地回答 yes or no。要根据具体情况来定。在 Java 中提供了一个
StringBuilder 类(这个类只在 J2SE5 及以上版本提供,以前的版本使用 StringBuffer 类),这个类也可以起到”+"的作用。那么我们应该用哪个呢?
下面让我们先看看如下的代码:
-
package string;
-
public class TestSimplePlus 4. {
-
public static void main(String[] args) 6. {
7.String s = “abc”;
8.String ss = “ok” + s + “xyz” + 5;
9.System.out.println(ss);
40 -
}
-
}
上面的代码将会输出正确的结果。从表面上看,对字符串和整型使用"+"号并没有什么区别,但事实真的如此吗? 下面让我们来看看这段代码的本质。
我们首先使用反编译工具(如 jdk 带的 javap、或 jad)将 TestSimplePlus 反编译成 Java Byte Code,其中的奥秘就一目了然了。在本文将使用 jad 来反编译,命令如下:
jad -o -a -s d.java TestSimplePlus.class
反编译后的代码如下:
-
package string;
-
import java.io.PrintStream; 4.
-
public class TestSimplePlus 6. {
-
public TestSimplePlus() 8. {
-
// 0 0:aload_0
10.// 1 1:invokespecial #8 <Method void Object()>
11.// 2 4:return -
}
-
public static void main(String args[])
-
{
-
String s = “abc”;
-
// 0 0:ldc1 #16 <String “abc”>
-
// 1 2:astore_1
19.String ss = (new StringBuilder(“ok”)).append(s).append(“xyz”).append(5).toString();
20.// 2 3:new #18 21. // 3 6:dup -
// 4 7:ldc1 #20 <String “ok”>
-
// 5 9:invokespecial #22 <Method void StringBuilder(String)> 24. // 6 12:aload_1
-
// 7 13:invokevirtual #25 <Method StringBuilder StringBuilder.append(String)> 26. // 8 16:ldc1 #29 <String “xyz”>
-
// 9 18:invokevirtual #25 <Method StringBuilder StringBuilder.append(String)> 28. // 10 21:iconst_5
-
// 11 22:invokevirtual #31 <Method StringBuilder StringBuilder.append(int)>
41 -
// 12 25:invokevirtual #34 <Method String StringBuilder.toString()> 31. // 13 28:astore_2
32.System.out.println(ss);
33.// 14 29:getstatic #38
读者可能看到上面的 Java 字节码感到迷糊,不过大家不必担心。本文的目的并不是讲解 Java Byte Code,因此, 并不用了解具体的字节码的含义。
使用 jad 反编译的好处之一就是可以同时生成字节码和源代码。这样可以进行对照研究。从上面的代码很容易看
出,虽然在源程序中使用了"+",但在编译时仍然将"+“转换成 StringBuilder。因此,我们可以得出结论,在 Java 中无论使用何种方式进行字符串连接,实际上都使用的是 StringBuilder。
那么是不是可以根据这个结论推出使用”+“和 StringBuilder 的效果是一样的呢?这个要从两个方面的解释。如果从运行结果来解释,那么”+“和 StringBuilder 是完全等效的。但如果从运行效率和资源消耗方面看,那它们将存在很大的区别。
当然,如果连接字符串行表达式很简单(如上面的顺序结构),那么”+"和 StringBuilder 基本是一样的,但如果结构比较复杂,如使用循环来连接字符串,那么产生的 Java Byte Code 就会有很大的区别。先让我们看看如下的代码:
42
- }
- System.out.println(s);
- }
- }
上面的代码返编译后的 Java Byte Code 如下:
- package string;
3.import java.io.PrintStream;
4.import java.util.Random; 5.
6. public class TestComplexPlus 7. {
8.
9. public TestComplexPlus()
10. {
11. // 0 0:aload_0
12.// 1 1:invokespecial #8 <Method void Object()>
13.// 2 4:return
14. }
15.
16. public static void main(String args[])
17. {
18. String s = “”;
19. // 0 0:ldc1 #16 <String “”>
20. // 1 2:astore_1
21.Random rand = new Random();
22.// 2 3:new #18 23. // 3 6:dup
24. // 4 7:invokespecial #20 <Method void Random()> 25. // 5 10:astore_2
26. for(int i = 0; i < 10; i++)
27. //* 6 11:iconst_0
28. //* 7 12:istore_3
29. //* 8 13:goto 49
30.s = (new StringBuilder(String.valueOf(s))).append(rand.nextInt(1000)).append(" ").t oString();
31.// 9 16:new #21 32. // 10 19:dup
33. // 11 20:aload_1
34.// 12 21:invokestatic #23 <Method String String.valueOf(Object)>
35.// 13 24:invokespecial #29 <Method void StringBuilder(String)>
43
-
// 14 27:aload_2
-
// 15 28:sipush 1000
38.// 16 31:invokevirtual #32 <Method int Random.nextInt(int)>
39.// 17 34:invokevirtual #36 <Method StringBuilder StringBuilder.append(int)> 40. // 18 37:ldc1 #40 <String " ">
41.// 19 39:invokevirtual #42 <Method StringBuilder StringBuilder.append(String)>
42.// 20 42:invokevirtual #45 <Method String StringBuilder.toString()> 43. // 21 45:astore_1 -
// 22 46:iinc 3 1
-
// 23 49:iload_3
-
// 24 50:bipush 10
-
// 25 52:icmplt 16
49.System.out.println(s);
50.// 26 55:getstatic #49
44
-
result.append(" ");
-
}
-
System.out.println(result.toString());
-
}
-
}
上面代码反编译后的结果如下: -
20.package string;
3.import java.io.PrintStream;
4.import java.util.Random; 5.
6. public class TestStringBuilder 7. {
8.
9. public TestStringBuilder()
10. {
11. // 0 0:aload_0
12.// 1 1:invokespecial #8 <Method void Object()>
13.// 2 4:return
14. }
15.
16. public static void main(String args[])
17. {
18. String s = “”;
19. // 0 0:ldc1 #16 <String “”>
20. // 1 2:astore_1
21.Random rand = new Random();
22.// 2 3:new #18 23. // 3 6:dup
24. // 4 7:invokespecial #20 <Method void Random()> 25. // 5 10:astore_2
26.StringBuilder result = new StringBuilder();
27.// 6 11:new #21 28. // 7 14:dup
29. // 8 15:invokespecial #23 <Method void StringBuilder()> 30. // 9 18:astore_3
31. for(int i = 0; i < 10; i++)
32. //* 10 19:iconst_0
33. //* 11 20:istore 4
34. //* 12 22:goto 47
35. {
36. result.append(rand.nextInt(1000));
45
- // 13 25:aload_3
- // 14 26:aload_2
- // 15 27:sipush 1000
40.// 16 30:invokevirtual #24 <Method int Random.nextInt(int)>
41.// 17 33:invokevirtual #28 <Method StringBuilder StringBuilder.append(int)> 42. // 18 36:pop -
result.append(" "); 44. // 19 37:aload_3
- // 20 38:ldc1 #32 <String " ">
- // 21 40:invokevirtual #34 <Method StringBuilder StringBuilder.append(String)> 47. // 22 43:pop
- }
- // 23 44:iinc 4 1
- // 24 47:iload 4
- // 25 49:bipush 10
- // 26 51:icmplt 25
54.System.out.println(result.toString());
55.// 27 54:getstatic #37
从上面的反编译结果可以看出,创建 StringBuilder 的代码被放在了 for 语句外。虽然这样处理在源程序中看起来复杂,但却换来了更高的效率,同时消耗的资源也更少了。
在使用 StringBuilder 时要注意,尽量不要"+"和 StringBuilder 混着用,否则会创建更多的 StringBuilder 对象,如下面代码所:
for (int i = 0; i < 10; i++)
{
result.append(rand.nextInt(1000)); result.append(" ");
}
改成如下形式:
for (int i = 0; i < 10; i++)
46
{
result.append(rand.nextInt(1000) + " ");
}
则反编译后的结果如下:
for(int i = 0; i < 10; i++)
//* 10 19:iconst_0
//* 11 20:istore 4
//* 12 22:goto 65
{
result.append((new StringBuilder(String.valueOf(rand.nextInt(1000)))).append(" ").toString());
// 13 25:aload_3
// 14 26:new #21
// 15 29:dup
从上面的代码可以看出,Java 编译器将"+“编译成了 StringBuilder,这样 for 语句每循环一次,又创建了一个
StringBuilder 对象。
如果将上面的代码在 JDK1.4 下编译,必须将 StringBuilder 改为 StringBuffer,而 JDK1.4 将”+"转换为StringBuffer(因为 JDK1.4 并没有提供 StringBuilder 类)。StringBuffer 和 StringBuilder 的功能基本一样,只是StringBuffer 是线程安全的,而 StringBuilder 不是线程安全的。因此,StringBuilder 的效率会更高。
6. 请说出下面程序的输出(2017-11-14-wl)
1.class StringEqualTest {
2.public static void main(String[] args) {
3.String s1 = “Programming”;
4.String s2 = new String(“Programming”);
5.String s3 = “Program”;
6.String s4 = “ming”;
7.String s5 = “Program” + “ming”;
8.String s6 = s3 + s4;
9.System.out.println(s1 == s2); //false
10.System.out.println(s1 == s5); //true
11.System.out.println(s1 == s6); //false
12.System.out.println(s1 == s6.intern()); //true
13.System.out.println(s2 == s2.intern()); //false
14. }
47
15. }
补充:解答上面的面试题需要知道如下两个知识点:
1.String 对象的 intern()方法会得到字符串对象在常量池中对应的版本的引用(如果常量池中有一个字符串与
String 对象的 equals 结果是 true),如果常量池中没有对应的字符串,则该字符串将被添加到常量池中,然后返回常量池中字符串的引用;
2.字符串的+操作其本质是创建了 StringBuilder 对象进行 append 操作,然后将拼接后的 StringBuilder 对象用 toString 方法处理成 String 对象,这一点可以用 javap -c StringEqualTest.class 命令获得 class 文件对应的 JVM 字节码指令就可以看出来。
7. Java 中的日期和时间(2017-11-19-wl)
7.1 如何取得年月日、小时分钟秒?(2017-11-19-wl)
1.public class DateTimeTest {
2.public static void main(String[] args) {
3.Calendar cal = Calendar.getInstance();
4.System.out.println(cal.get(Calendar.YEAR));
5.System.out.println(cal.get(Calendar.MONTH)); // 0 - 11
6.System.out.println(cal.get(Calendar.DATE));
7.System.out.println(cal.get(Calendar.HOUR_OF_DAY));
8.System.out.println(cal.get(Calendar.MINUTE));
9.System.out.println(cal.get(Calendar.SECOND));
10.// Java 8
11.LocalDateTime dt = LocalDateTime.now();
12.System.out.println(dt.getYear());
13.System.out.println(dt.getMonthValue()); // 1 - 12
14.System.out.println(dt.getDayOfMonth());
15.System.out.println(dt.getHour());
16.System.out.println(dt.getMinute());
17.System.out.println(dt.getSecond());
18. }
19. }
7.2 如何取得从 1970 年 1 月 1 日 0 时 0 分 0 秒到现在的毫秒数?(2017-11-19-wl)
1.Calendar.getInstance().getTimeInMillis(); //第一种方式
2.System.currentTimeMillis(); //第二种方式
3.// Java 8
4.Clock.systemDefaultZone().millis();
7.3 如何取得某月的最后一天?(2017-11-19-wl)
1.//获取当前月第一天:
2.Calendar c = Calendar.getInstance();
3.c.add(Calendar.MONTH, 0);
4.c.set(Calendar.DAY_OF_MONTH,1);//设置为 1 号,当前日期既为本月第一天
5.String first = format.format(c.getTime());
6.System.out.println("===============first:"+first); 7.
8.//获取当前月最后一天
9.Calendar ca = Calendar.getInstance();
10.ca.set(Calendar.DAY_OF_MONTH, ca.getActualMaximum(Calendar.DAY_OF_MONTH));
11.String last = format.format(ca.getTime());
12.System.out.println("===============last:"+last); 13.
14.//Java 8
15.LocalDate today = LocalDate.now();
16.//本月的第一天
17.LocalDate firstday = LocalDate.of(today.getYear(),today.getMonth(),1);
18.//本月的最后一天
19.LocalDate lastDay =today.with(TemporalAdjusters.lastDayOfMonth());
20.System.out.println(“本月的第一天”+firstday);
21.System.out.println(“本月的最后一天”+lastDay);
7.4 如何格式化日期?(2017-11-19-wl)
1)Java.text.DataFormat 的子类(如 SimpleDateFormat 类)中的 format(Date)方法可将日期格式化。2)Java 8 中可以用 java.time.format.DateTimeFormatter 来格式化时间日期,代码如下所示:
1.import java.text.SimpleDateFormat;
2.import java.time.LocalDate;
3.import java.time.format.DateTimeFormatter;
4.import java.util.Date;
5.class DateFormatTest { 6.
7.public static void main(String[] args) {
8. SimpleDateFormat oldFormatter = new SimpleDateFormat(“yyyy/MM/dd”);
9.Date date1 = new Date();
10.System.out.println(oldFormatter.format(date1)); 11.
12. // Java 8
13.DateTimeFormatter newFormatter = DateTimeFormatter.ofPattern(“yyyy/MM/dd”);
14.LocalDate date2 = LocalDate.now();
15.System.out.println(date2.format(newFormatter));
16. }
17. }
补充:Java 的时间日期 API 一直以来都是被诟病的东西,为了解决这一问题,Java 8 中引入了新的时间日期 API, 其中包括 LocalDate、LocalTime、LocalDateTime、Clock、Instant 等类,这些的类的设计都使用了不变模式,因此是线程安全的设计。
7.5 打印昨天的当前时刻? (2017-11-19-wl)
1.import java.util.Calendar;
2.class YesterdayCurrent {
3.public static void main(String[] args){
4.Calendar cal = Calendar.getInstance();
5.cal.add(Calendar.DATE, -1);
6.System.out.println(cal.getTime()); 7. }
50
- }
11.//java-8
12.import java.time.LocalDateTime;
13.class YesterdayCurrent {
14.public static void main(String[] args) {
15.LocalDateTime today = LocalDateTime.now();
16.LocalDateTime yesterday = today.minusDays(1);
17.System.out.println(yesterday);
18. }
19. }
7.6 Java8 的日期特性? (2017-12-3-wl)
Java 8 日期/时间特性
Java 8 日期/时间 API 是 JSR-310 的实现,它的实现目标是克服旧的日期时间实现中所有的缺陷,新的日期/时间
API 的一些设计原则是:
不变性:新的日期/时间 API 中,所有的类都是不可变的,这对多线程环境有好处。
关注点分离:新的 API 将人可读的日期时间和机器时间(unix timestamp)明确分离,它为日期(Date)、时间
(Time)、日期时间(DateTime)、时间戳(unix timestamp)以及时区定义了不同的类。
清晰:在所有的类中,方法都被明确定义用以完成相同的行为。举个例子,要拿到当前实例我们可以使用 now()方法,在所有的类中都定义了 format()和 parse()方法,而不是像以前那样专门有一个独立的类。为了更好的处理问题,所有的类都使用了工厂模式和策略模式,一旦你使用了其中某个类的方法,与其他类协同工作并不困难。
实用操作:所有新的日期/时间 API 类都实现了一系列方法用以完成通用的任务,如:加、减、格式化、解析、从日期/时间中提取单独部分,等等。
可扩展性:新的日期/时间 API 是工作在 ISO-8601 日历系统上的,但我们也可以将其应用在非 ISO 的日历上。
51
Java 8 日期/时间 API 包解释
java.time 包:这是新的 Java 日期/时间 API 的基础包,所有的主要基础类都是这个包的一部分,如:LocalDate, LocalTime, LocalDateTime, Instant, Period, Duration 等等。所有这些类都是不可变的和线程安全的,在绝大多数情况下,这些类能够有效地处理一些公共的需求。
java.time.chrono 包:这个包为非 ISO 的日历系统定义了一些泛化的 API,我们可以扩展 AbstractChronology
类来创建自己的日历系统。
java.time.format 包:这个包包含能够格式化和解析日期时间对象的类,在绝大多数情况下,我们不应该直接使用它们,因为 java.time 包中相应的类已经提供了格式化和解析的方法。
java.time.temporal 包:这个包包含一些时态对象,我们可以用其找出关于日期/时间对象的某个特定日期或时间, 比如说,可以找到某月的第一天或最后一天。你可以非常容易地认出这些方法,因为它们都具有“withXXX”的格 式。
java.time.zone 包:这个包包含支持不同时区以及相关规则的类。
Java 8 日期/时间常用 API
1.java.time.LocalDate
LocalDate 是一个不可变的类,它表示默认格式(yyyy-MM-dd)的日期,我们可以使用 now()方法得到当前时间, 也可以提供输入年份、月份和日期的输入参数来创建一个 LocalDate 实例。该类为 now()方法提供了重载方法,我们可以传入 ZoneId 来获得指定时区的日期。该类提供与 java.sql.Date 相同的功能,对于如何使用该类,我们来看一个简单的例子。
package com.journaldev.java8.time;
import java.time.LocalDate;
52
import java.time.Month; import java.time.ZoneId;
/**
*LocalDate Examples
*@author pankaj
*
*/
public class LocalDateExample {
public static void main(String[] args) {
//Current Date
LocalDate today = LocalDate.now(); System.out.println(“Current Date=”+today);
//Creating LocalDate by providing input arguments
LocalDate firstDay_2014 = LocalDate.of(2014, Month.JANUARY, 1); System.out.println(“Specific Date=”+firstDay_2014);
//Try creating date by providing invalid inputs
//LocalDate feb29_2014 = LocalDate.of(2014, Month.FEBRUARY, 29);
//Exception in thread “main” java.time.DateTimeException:
//Invalid date ‘February 29’ as ‘2014’ is not a leap year
//Current date in “Asia/Kolkata”, you can get it from ZoneId javadoc LocalDate todayKolkata = LocalDate.now(ZoneId.of(“Asia/Kolkata”)); System.out.println(“Current Date in IST=”+todayKolkata);
//java.time.zone.ZoneRulesException: Unknown time-zone ID: IST
//LocalDate todayIST = LocalDate.now(ZoneId.of(“IST”));
//Getting date from the base date i.e 01/01/1970 LocalDate dateFromBase = LocalDate.ofEpochDay(365);
System.out.println("365th day from base date= "+dateFromBase);
LocalDate hundredDay2014 = LocalDate.ofYearDay(2014, 100); System.out.println(“100th day of 2014=”+hundredDay2014);
}
}
输出:
Current Date=2014-04-28
53
Specific Date=2014-01-01 Current Date in IST=2014-04-29
365th day from base date= 1971-01-01
100th day of 2014=2014-04-10
2.java.time.LocalTime
LocalTime 是一个不可变的类,它的实例代表一个符合人类可读格式的时间,默认格式是 hh:mm:ss.zzz。像LocalDate 一样,该类也提供了时区支持,同时也可以传入小时、分钟和秒等输入参数创建实例,我们来看一个简单的程序,演示该类的使用方法。
package com.journaldev.java8.time;
import java.time.LocalTime; import java.time.ZoneId;
/**
- LocalTime Examples
*/
public class LocalTimeExample {
public static void main(String[] args) {
//Current Time
LocalTime time = LocalTime.now(); System.out.println(“Current Time=”+time);
//Creating LocalTime by providing input arguments LocalTime specificTime = LocalTime.of(12,20,25,40); System.out.println(“Specific Time of Day=”+specificTime);
//Try creating time by providing invalid inputs
//LocalTime invalidTime = LocalTime.of(25,20);
//Exception in thread “main” java.time.DateTimeException:
//Invalid value for HourOfDay (valid values 0 - 23): 25
//Current date in “Asia/Kolkata”, you can get it from ZoneId javadoc LocalTime timeKolkata = LocalTime.now(ZoneId.of(“Asia/Kolkata”)); System.out.println(“Current Time in IST=”+timeKolkata);
//java.time.zone.ZoneRulesException: Unknown time-zone ID: IST
//LocalTime todayIST = LocalTime.now(ZoneId.of(“IST”));
//Getting date from the base date i.e 01/01/1970
54
LocalTime specificSecondTime = LocalTime.ofSecondOfDay(10000); System.out.println("10000th second time= "+specificSecondTime);
}
}
输出:
Current Time=15:51:45.240
Specific Time of Day=12:20:25.000000040 Current Time in IST=04:21:45.276 10000th second time= 02:46:40
3.java.time.LocalDateTime
LocalDateTime 是一个不可变的日期-时间对象,它表示一组日期-时间,默认格式是 yyyy-MM-dd-HH-mm-
ss.zzz。它提供了一个工厂方法,接收 LocalDate 和 LocalTime 输入参数,创建 LocalDateTime 实例。我们来看一个简单的例子。
System.out.println(“Specific Date=”+specificDate);
//Try creating date by providing invalid inputs
//LocalDateTime feb29_2014 = LocalDateTime.of(2014, Month.FEBRUARY, 28, 25,1,1);
//Exception in thread “main” java.time.DateTimeException:
//Invalid value for HourOfDay (valid values 0 - 23): 25
//Current date in “Asia/Kolkata”, you can get it from ZoneId javadoc LocalDateTime todayKolkata = LocalDateTime.now(ZoneId.of(“Asia/Kolkata”)); System.out.println(“Current Date in IST=”+todayKolkata);
//java.time.zone.ZoneRulesException: Unknown time-zone ID: IST
//LocalDateTime todayIST = LocalDateTime.now(ZoneId.of(“IST”));
//Getting date from the base date i.e 01/01/1970
LocalDateTime dateFromBase = LocalDateTime.ofEpochSecond(10000, 0, ZoneOffset.UTC); System.out.println("10000th second time from 01/01/1970= "+dateFromBase);
}
}
输出:
Current DateTime=2014-04-28T16:00:49.455 Current DateTime=2014-04-28T16:00:49.493 Specific Date=2014-01-01T10:10:30
Current Date in IST=2014-04-29T04:30:49.493
10000th second time from 01/01/1970= 1970-01-01T02:46:40
在所有这三个例子中, 我们已经看到如果我们提供了无效的参数去创建日期 / 时间, 那么系统会抛出
java.time.DateTimeException,这是一种运行时异常,我们并不需要显式地捕获它。
同时我们也看到,能够通过传入 ZoneId 得到日期/时间数据,你可以从它的 Javadoc 中得到支持的 Zoneid 的列表,当运行以上类时,可以得到以上输出。
4.java.time.Instant
Instant 类是用在机器可读的时间格式上的,它以 Unix 时间戳的形式存储日期时间,我们来看一个简单的程序
package com.journaldev.java8.time;
import java.time.Duration; import java.time.Instant;
public class InstantExample {
public static void main(String[] args) {
//Current timestamp
Instant timestamp = Instant.now(); System.out.println("Current Timestamp = "+timestamp);
//Instant from timestamp
Instant specificTime = Instant.ofEpochMilli(timestamp.toEpochMilli()); System.out.println("Specific Time = "+specificTime);
//Duration example
Duration thirtyDay = Duration.ofDays(30); System.out.println(thirtyDay);
}
}
输出:
Current Timestamp = 2014-04-28T23:20:08.489Z Specific Time = 2014-04-28T23:20:08.489Z
PT720H
5.日期 API 工具
我们早些时候提到过,大多数日期/时间 API 类都实现了一系列工具方法,如:加/减天数、周数、月份数,等等。还有其他的工具方法能够使用 TemporalAdjuster 调整日期,并计算两个日期间的周期。
//Compare two LocalDate for before and after
System.out.println("Today is before 01/01/2015? "+today.isBefore(LocalDate.of(2015,1,1)));
//Create LocalDateTime from LocalDate
System.out.println(“Current Time=”+today.atTime(LocalTime.now()));
//plus and minus operations
System.out.println("10 days after today will be "+today.plusDays(10)); System.out.println("3 weeks after today will be "+today.plusWeeks(3)); System.out.println("20 months after today will be "+today.plusMonths(20));
System.out.println("10 days before today will be "+today.minusDays(10)); System.out.println("3 weeks before today will be "+today.minusWeeks(3)); System.out.println("20 months before today will be "+today.minusMonths(20));
//Temporal adjusters for adjusting the dates System.out.println("First date of this month= "+today. with(TemporalAdjusters.firstDayOfMonth()));
LocalDate lastDayOfYear = today.with(TemporalAdjusters.lastDayOfYear()); System.out.println("Last date of this year= "+lastDayOfYear);
Period period = today.until(lastDayOfYear); System.out.println("Period Format= "+period);
System.out.println("Months remaining in the year= "+period.getMonths());
}
}
输出:
Year 2014 is Leap Year? false Today is before 01/01/2015? true
Current Time=2014-04-28T16:23:53.154
10 days after today will be 2014-05-08
3 weeks after today will be 2014-05-19
20 months after today will be 2015-12-28
10 days before today will be 2014-04-18
3 weeks before today will be 2014-04-07
20 months before today will be 2012-08-28 First date of this month= 2014-04-01
Last date of this year= 2014-12-31 Period Format= P8M3D
Months remaining in the year= 8
6.解析和格式化
将一个日期格式转换为不同的格式,之后再解析一个字符串,得到日期时间对象,这些都是很常见的。我们来看一下简单的例子。
package com.journaldev.java8.time;
import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class DateParseFormatExample {
public static void main(String[] args) {
//Format examples
LocalDate date = LocalDate.now();
//default format
System.out.println(“Default format of LocalDate=”+date);
//specific format System.out.println(date.format(DateTimeFormatter.ofPattern(“d::MMM::uuuu”))); System.out.println(date.format(DateTimeFormatter.BASIC_ISO_DATE));
LocalDateTime dateTime = LocalDateTime.now();
//default format
System.out.println(“Default format of LocalDateTime=”+dateTime);
//specific format System.out.println(dateTime.format(DateTimeFormatter.ofPattern(“d::MMM::uuuu HH::mm::ss”)));
System.out.println(dateTime.format(DateTimeFormatter.BASIC_ISO_DATE));
Instant timestamp = Instant.now();
//default format
System.out.println(“Default format of Instant=”+timestamp);
//Parse examples
LocalDateTime dt = LocalDateTime.parse(“27::Apr::2014 21::39::48”, DateTimeFormatter.ofPattern(“d::MMM::uuuu HH::mm::ss”));
System.out.println("Default format after parsing = "+dt);
}
}
输出:
Default format of LocalDate=2014-04-28
59
28::Apr::2014
20140428
Default format of LocalDateTime=2014-04-28T16:25:49.341 28::Apr::2014 16::25::49
20140428
Default format of Instant=2014-04-28T23:25:49.342Z Default format after parsing = 2014-04-27T21:39:48
7.旧的日期时间支持
旧的日期/时间类已经在几乎所有的应用程序中使用,因此做到向下兼容是必须的。这也是为什么会有若干工具方法帮助我们将旧的类转换为新的类,反之亦然。我们来看一下简单的例子。
System.out.println(defaultZone);
//ZonedDateTime from specific Calendar
ZonedDateTime gregorianCalendarDateTime = new GregorianCalendar().toZonedDateTime(); System.out.println(gregorianCalendarDateTime);
//Date API to Legacy classes
Date dt = Date.from(Instant.now()); System.out.println(dt);
TimeZone tz = TimeZone.getTimeZone(defaultZone); System.out.println(tz);
GregorianCalendar gc = GregorianCalendar.from(gregorianCalendarDateTime); System.out.println(gc);
}
}
输出:
Date = 2014-04-28T16:28:54.340 2014-04-28T23:28:54.395Z
America/Los_Angeles
2014-04-28T16:28:54.404-07:00[America/Los_Angeles]
Mon Apr 28 16:28:54 PDT 2014
sun.util.calendar.ZoneInfo[id=“America/Los_Angeles”,offset=- 28800000,dstSavings=3600000,useDaylight=true,transitions=185,lastRule=java.util.SimpleTimeZone[id=A merica/Los_Angeles,offset=- 28800000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=3,startMonth=2,startDay=8,startD ayOfWeek=1,startTime=7200000,startTimeMode=0,endMode=3,endMonth=10,endDay=1,endDayOfWeek=1,endTime= 7200000,endTimeMode=0]]
java.util.GregorianCalendar[time=1398727734404,areFieldsSet=true,areAllFieldsSet=true,lenient=t rue,zone=sun.util.calendar.ZoneInfo[id=“America/Los_Angeles”,offset=- 28800000,dstSavings=3600000,useDaylight=true,transitions=185,lastRule=java.util.SimpleTimeZone[id=A merica/Los_Angeles,offset=- 28800000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=3,startMonth=2,startDay=8,startD ayOfWeek=1,startTime=7200000,startTimeMode=0,endMode=3,endMonth=10,endDay=1,endDayOfWeek=1,endTime= 7200000,endTimeMode=0]],firstDayOfWeek=2,minimalDaysInFirstWeek=4,ERA=1,YEAR=2014,MONTH=3,WEEK_OF_Y EAR=18,WEEK_OF_MONTH=5,DAY_OF_MONTH=28,DAY_OF_YEAR=118,DAY_OF_WEEK=2,DAY_OF_WEEK_IN_MONTH=4,AM_PM=1
,HOUR=4,HOUR_OF_DAY=16,MINUTE=28,SECOND=54,MILLISECOND=404,ZONE_OFFSET=-28800000,DST_OFFSET=3600000]
补充:我们可以看到,旧的 TimeZone 和 GregorianCalendar 类的 toString()方法太啰嗦了,一点都不友好。
7.7 Java8 之前的日期和时间使用的槽点 (2017-11-19-wl)
Tiago Fernandez 做过一次投票,选举最烂的 JAVA API,排第一的 EJB2.X,第二的就是日期 API(Date 和
Calender)
1.槽点一
最开始的时候,Date 既要承载日期信息,又要做日期之间的转换,还要做不同日期格式的显示,职责较繁杂(不懂单一职责,你妈妈知道吗?纯属恶搞~哈哈)
后来从 JDK 1.1 开始,这三项职责分开了:
1)使用 Calendar 类实现日期和时间字段之间转换;
2)使用 DateFormat 类来格式化和分析日期字符串;
3)而 Date 只用来承载日期和时间信息。
原有 Date 中的相应方法已废弃。不过,无论是 Date,还是 Calendar,都用着太不方便了,这是 API 没有设计好的地方。
2.槽点二
坑爹的 year 和 month。
我们看下面的代码:
1.Date date = new Date(2012,1,1);
2.System.out.println(date);
输 出 Thu Feb 01 00:00:00 CST 3912
观察输出结果,year 是 2012+1900,而 month,月份参数我不是给了 1 吗?怎么输出二月(Feb)了? 应该曾有人告诉你,如果你要设置日期,应该使用 java.util.Calendar,像这样…
1. Calendar calendar = Calendar.getInstance();
62
2. calendar.set(2013, 8, 2);
这样写又不对了,calendar 的 month 也是从 0 开始的,表达 8 月份应该用 7 这个数字,要么就干脆用枚举
1. calendar.set(2013, Calendar.AUGUST, 2);
注意上面的代码,Calendar 年份的传值不需要减去 1900(当然月份的定义和 Date 还是一样),这种不一致真
是让人抓狂!有些人可能知道,Calendar 相关的 API 是 IBM 捐出去的,所以才导致不一致。
3.槽点三
java.util.Date 与 java.util.Calendar 中的所有属性都是可变的下面的代码,计算两个日期之间的天数…
1.public static void main(String[] args) {
2.Calendar birth = Calendar.getInstance();
3.birth.set(1975, Calendar.MAY, 26);
4.Calendar now = Calendar.getInstance();
5.System.out.println(daysBetween(birth, now));
6.System.out.println(daysBetween(birth, now)); // 显示 0? 7. }
8.
9.public static long daysBetween(Calendar begin, Calendar end) {
10.long daysBetween = 0;
11.while(begin.before(end)) {
12.begin.add(Calendar.DAY_OF_MONTH, 1);
13.daysBetween++;
14. }
15. return daysBetween;
16. }
daysBetween 有点问题,如果连续计算两个 Date 实例的话,第二次会取得 0,因为 Calendar 状态是可变的,考虑到重复计算的场合,最好复制一个新的 Calendar
9. }
以上种种,导致目前有些第三方的 java 日期库诞生,比如广泛使用的 JODA-TIME,还有 Date4j 等,虽然第三方库已经足 3 / 8 够强大,好用,但还是有兼容问题的,比如标准的 JSF 日期转换器与 joda-time API 就不兼容,你需要编写自己的转换器,所以标准的 API 还是必须的,于是就有了 JSR310。
7.8 Java8 日期实现 JSR310 规范 (2017-11-23-wl)
1.JSR310 介绍
JSR 310 实际上有两个日期概念。第一个是 Instant,它大致对应于 java.util.Date 类,因为它代表了一个确定的时间点,即相对于标准 Java 纪元(1970 年 1 月 1 日)的偏移量;但与 java.util.Date 类不同的是其精确到了纳秒级别。
第二个对应于人类自身的观念,比如 LocalDate 和 LocalTime。他们代表了一般的时区概念,要么是日期(不包含时间),要么是时间(不包含日期),类似于 java.sql 的表示方式。此外,还有一个 MonthDay,它可以存储某人的生日(不包含年份)。每个类都在内部存储正确的数据而不是像 java.util.Date 那样利用午夜 12 点来区分日期,利用 1970-01-01 来表示时间。
目前 Java8 已经实现了 JSR310 的全部内容。新增了 java.time 包定义的类表示了日期-时间概念的规则,包括 instants,durations, dates, times, time-zones and periods。这些都是基于 ISO 日历系统,它又是遵循
Gregorian 规则的。最重要的一点是值不可变,且线程安全,通过下面一张图,我们快速看下 java.time 包下的一些主要的类的值的格式,方便理解。
2.Java8 方法概览
java.time 包下的方法概览
方法名 说明
Of 静态工厂方法
parse 静态工厂方法,关注于解析
get 获取某些东西的值
is 检查某些东西的是否是 true
with 不可变的 setter 等价物
plus 加一些量到某个对象
minus 从某个对象减去一些量
to 转换到另一个类型
at 把这个对象与另一个对象组合起来
与旧的 API 相比
3.简单实用 java.time 的 API 实用
1.public class TimeIntroduction {
2.public static void testClock() throws InterruptedException {
3.//时钟提供给我们用于访问某个特定 时区的 瞬时时间、日期 和 时间的。
4.Clock c1 = Clock.systemUTC(); //系统默认 UTC 时钟(当前瞬时时间 System.currentTimeMillis())
5.System.out.println(c1.millis()); //每次调用将返回当前瞬时时间(UTC)
6.Clock c2 = Clock.systemDefaultZone(); //系统默认时区时钟(当前瞬时时间)
7.Clock c31 = Clock.system(ZoneId.of(“Europe/Paris”)); //巴黎时区
8.System.out.println(c31.millis()); //每次调用将返回当前瞬时时间(UTC)
9.Clock c32 = Clock.system(ZoneId.of(“Asia/Shanghai”));//上海时区
10.System.out.println(c32.millis());//每次调用将返回当前瞬时时间(UTC)
11.Clock c4 = Clock.fixed(Instant.now(), ZoneId.of(“Asia/Shanghai”));//固定上海时区时钟
12.System.out.println(c4.millis());
13.Thread.sleep(1000); 14.
66
15.System.out.println(c4.millis()); //不变 即时钟时钟在那一个点不动
16.Clock c5 = Clock.offset(c1, Duration.ofSeconds(2)); //相对于系统默认时钟两秒的时钟
17.System.out.println(c1.millis());
18.System.out.println(c5.millis());
19. }
20.public static void testInstant() {
21.//瞬时时间 相当于以前的 System.currentTimeMillis()
22.Instant instant1 = Instant.now();
23.System.out.println(instant1.getEpochSecond());//精确到秒 得到相对于 1970-01-01 00:00:00
24.UTC 的一个时间
25.System.out.println(instant1.toEpochMilli()); //精确到毫秒
26.Clock clock1 = Clock.systemUTC(); //获取系统 UTC 默认时钟
27.Instant instant2 = Instant.now(clock1);//得到时钟的瞬时时间
28.System.out.println(instant2.toEpochMilli());
29.Clock clock2 = Clock.fixed(instant1, ZoneId.systemDefault()); //固定瞬时时间时钟
30.Instant instant3 = Instant.now(clock2);//得到时钟的瞬时时间
31.System.out.println(instant3.toEpochMilli());//equals instant1
32. }
33.public static void testLocalDateTime() {
34.//使用默认时区时钟瞬时时间创建 Clock.systemDefaultZone() -->即相对于 ZoneId.systemDefault()
35.默认时区
36.LocalDateTime now = LocalDateTime.now();
37.System.out.println(now);
38.//自定义时区
39.LocalDateTime now2 = LocalDateTime.now(ZoneId.of(“Europe/Paris”));
40.System.out.println(now2);//会以相应的时区显示日期
41.//自定义时钟
42.Clock clock = Clock.system(ZoneId.of(“Asia/Dhaka”));
43.LocalDateTime now3 = LocalDateTime.now(clock);
44.System.out.println(now3);//会以相应的时区显示日期
45.//不需要写什么相对时间 如 java.util.Date 年是相对于 1900 月是从 0 开始
46. //2013-12-31 23:59
47.LocalDateTime d1 = LocalDateTime.of(2013, 12, 31, 23, 59);
48.//年月日 时分秒 纳秒
49.LocalDateTime d2 = LocalDateTime.of(2013, 12, 31, 23, 59, 59, 11);
50.//使用瞬时时间 + 时区
51.Instant instant = Instant.now();
52.LocalDateTime d3 = LocalDateTime.ofInstant(Instant.now(), ZoneId.systemDefault());
53.System.out.println(d3);
54.//解析 String—>LocalDateTime
55.LocalDateTime d4 = LocalDateTime.parse(“2013-12-31T23:59”);
56.System.out.println(d4);
57.LocalDateTime d5 = LocalDateTime.parse(“2013-12-31T23:59:59.999”);//999 毫秒 等价于
67
- 999000000 纳秒
60.System.out.println(d5);
61.//使用 DateTimeFormatter API 解析 和 格式化
62.DateTimeFormatter formatter = DateTimeFormatter.ofPattern(“yyyy/MM/dd HH:mm:ss”);
63.LocalDateTime d6 = LocalDateTime.parse(“2013/12/31 23:59:59”, formatter);
64.System.out.println(formatter.format(d6));
65.//时间获取
66.System.out.println(d6.getYear());
67.System.out.println(d6.getMonth());
68.System.out.println(d6.getDayOfYear());
69.System.out.println(d6.getDayOfMonth());
70.System.out.println(d6.getDayOfWeek());
71.System.out.println(d6.getHour());
72.System.out.println(d6.getMinute());
73.System.out.println(d6.getSecond());
74.System.out.println(d6.getNano());
75.//时间增减
76.LocalDateTime d7 = d6.minusDays(1);
77.LocalDateTime d8 = d7.plus(1, IsoFields.QUARTER_YEARS);
78.//LocalDate 即年月日 无时分秒
79.//LocalTime 即时分秒 无年月日
80.//API 和 LocalDateTime 类似就不演示了
81. }
82.public static void testZonedDateTime() {
83.//即带有时区的 date-time 存储纳秒、时区和时差(避免与本地 date-time 歧义)。
84.//API 和 LocalDateTime 类似,只是多了时差(如 2013-12-20T10:35:50.711+08:00[Asia/Shanghai])
85.ZonedDateTime now = ZonedDateTime.now();
86.System.out.println(now);
87.ZonedDateTime now2 = ZonedDateTime.now(ZoneId.of(“Europe/Paris”));
88.System.out.println(now2);
89.//其他的用法也是类似的 就不介绍了
90.ZonedDateTime z1 = ZonedDateTime.parse(“2013-12-31T23:59:59Z[Europe/Paris]”);
91.System.out.println(z1);
92. }
93.public static void testDuration() {
94.//表示两个瞬时时间的时间段
95.Duration d1 = Duration.between(Instant.ofEpochMilli(System.currentTimeMillis() - 12323123),
96.Instant.now())
97. ;
98.//得到相应的时差
99.System.out.println(d1.toDays());
100.System.out.println(d1.toHours());
- System.out.println(d1.toMinutes()); 102.
103.System.out.println(d1.toMillis());
104.System.out.println(d1.toNanos());
105.//1 天时差 类似的还有如 ofHours()
106.Duration d2 = Duration.ofDays(1);
107.System.out.println(d2.toDays()); - }
109.public static void testChronology() {
110.//提供对 java.util.Calendar 的替换,提供对年历系统的支持
111.Chronology c = HijrahChronology.INSTANCE;
112.ChronoLocalDateTime d = c.localDateTime(LocalDateTime.now());
113.System.out.println(d); - } 115. /**
-
- 新旧日期转换
- */
118.public static void testNewOldDateConversion(){
119.Instant instant=new Date().toInstant();
120.Date date=Date.from(instant);
121.System.out.println(instant);
122.System.out.println(date); - }
124.public static void main(String[] args) throws InterruptedException {
125.testClock();
126.testInstant();
127.testLocalDateTime();
128.testZonedDateTime();
129.testDuration();
130.testChronology();
131.testNewOldDateConversion(); - }
- }
7.9JSR310 规范 Joda-Time 的区别 (2017-11-23-wl)
其实 JSR310 的规范领导者 Stephen Colebourne,同时也是 Joda-Time 的创建者,JSR310 是在 Joda-
Time 的基础上建立的,参考了绝大部分的 API,但并不是说 JSR310=JODA-Time,下面几个比较明显的区别是:
1.最明显的变化就是包名(从 org.joda.time 以及 java.time)
2.JSR310 不接受 NULL 值,Joda-Time 视 NULL 值为 0
3.JSR310 的计算机相关的时间(Instant)和与人类相关的时间(DateTime)之间的差别变得更明显
4.JSR310 所有抛出的异常都是 DateTimeException 的子类。虽然 DateTimeException 是一个
RuntimeException
7.10 总 结 (2017-11-23-wl)
Java.time java.util.Calendar 以 及 Date
流畅的 API 不流畅的 API
实例不可变 实例可变
线程安全 非线程安全
六、Java 的数据类型
1.Java 的基本数据类型都有哪些各占几个字节
如下表所示:
四类 八种 字节数 数据表示范围
整型
byte 1 -128~127
short 2 -32768~32767
int 4 -2147483648~2147483647
long 8 -263~263-1
浮点型 float 4 -3.403E38~3.403E38
double 8 -1.798E308~1.798E308
字符型 char 2 表示一个字符,如(‘a’,‘A’,‘0’,‘家’)
布尔型 boolean 1 只有两个值 true 与 false
2.String 是基本数据类型吗?(2017-11-12-wl)
String 是引用类型,底层用 char 数组实现的。
- short s1 = 1; s1 = s1 + 1; 有错吗?short s1 = 1; s1 += 1 有错吗;(2017- 11-12-wl)
前者不正确,后者正确。对于 short s1 = 1; s1 = s1 + 1;由于 1 是 int 类型,因此 s1+1 运算结果也是 int 型, 需要强制转换类型才能赋值给 short 型。而 short s1 = 1; s1 += 1;可以正确编译,因为 s1+= 1;相当于 s1 = (short)(s1 + 1);其中有隐含的强制类型转换。
4.int 和 和 Integer 有什么区别?(2017-11-12-wl)
Java 是一个近乎纯洁的面向对象编程语言,但是为了编程的方便还是引入了基本数据类型,为了能够将这些基本数据类型当成对象操作,Java 为每一个基本数据类型都引入了对应的包装类型(wrapper class),int 的包装类就是Integer,从 Java 5 开始引入了自动装箱/拆箱机制,使得二者可以相互转换。
Java 为每个原始类型提供了包装类型:
-原始类型: boolean,char,byte,short,int,long,float,double
-包装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double
5.下面 Integer 类型的数值比较输出的结果为?(2017-11-12-wl)
如果不明就里很容易认为两个输出要么都是 true 要么都是 false。首先需要注意的是 f1、f2、f3、f4 四个变量都是 Integer 对象引用,所以下面的==运算比较的不是值而是引用。装箱的本质是什么呢?当我们给一个 Integer 对象赋一个 int 值的时候,会调用 Integer 类的静态方法 valueOf,如果看看 valueOf 的源代码就知道发生了什么。
源码:
IntegerCache 是 Integer 的内部类,其代码如下所示:
简单的说,如果整型字面量的值在-128 到 127 之间,那么不会 new 新的 Integer 对象,而是直接引用常量池中的 Integer 对象,所以上面的面试题中 f1f2 的结果是 true,而 f3f4 的结果是 false。
提醒:越是貌似简单的面试题其中的玄机就越多,需要面试者有相当深厚的功力。
6.String 类常用方法(2017-11-15-lyq)
7.String、StringBuffer、StringBuilder 的区别?(2017-11-23-wzz)
(1) 、 可 变 不 可 变 String:字符串常量,在修改时不会改变自身;若修改,等于重新生成新的字符串对象。 StringBuffer:在修改时会改变对象自身,每次操作都是对 StringBuffer 对象本身进行修改,不是生成新的对
象;使用场景:对字符串经常改变情况下,主要方法:append(),insert()等。
(2)、线程是否安全
String:对象定义后不可变,线程安全。
StringBuffer:是线程安全的(对调用方法加入同步锁),执行效率较慢,适用于多线程下操作字符串缓冲区大量数据。
StringBuilder:是线程不安全的,适用于单线程下操作字符串缓冲区大量数据。
(3)、共同点
StringBuilder 与 StringBuffer 有公共父类 AbstractStringBuilder(抽象类)。
StringBuilder、StringBuffer 的方法都会调用 AbstractStringBuilder 中的公共方法,如 super.append(…)。只是 StringBuffer 会在方法上加 synchronized 关键字,进行同步。最后,如果程序不是多线程的,那么使用
StringBuilder 效率高于 StringBuffer。
- 数据类型之间的转换(2017-11-23-wzz)
(1)、字符串如何转基本数据类型?
调用基本数据类型对应的包装类中的方法 parseXXX(String)或 valueOf(String)即可返回相应基本类型。
(2)、基本数据类型如何转字符串?
一种方法是将基本数据类型与空字符串(“”)连接(+)即可获得其所对应的字符串;另一种方法是调用 String
类中的 valueOf()方法返回相应字符串。
七、Java 的 IO
1.Java 中有几种类型的流(2017-11-23-wzz)
按照流的方向:输入流(inputStream)和输出流(outputStream)。
按照实现功能分:节点流(可以从或向一个特定的地方(节点)读写数据。如 FileReader)和处理流(是对一个已存在的流的连接和封装,通过所封装的流的功能调用实现数据读写。如 BufferedReader。处理流的构造方法总是要带一个其他的流对象做参数。一个流对象经过其他流的多次包装,称为流的链接。)
按照处理数据的单位: 字节流和字符流。字节流继承于 InputStream 和 OutputStream, 字符流继承于
InputStreamReader 和 OutputStreamWriter 。
2.字节流如何转为字符流
字节输入流转字符输入流通过 InputStreamReader 实现,该类的构造函数可以传入 InputStream 对象。
字节输出流转字符输出流通过 OutputStreamWriter 实现,该类的构造函数可以传入 OutputStream 对象。
3.如何将一个 java 对象序列化到文件里
在 java 中能够被序列化的类必须先实现 Serializable 接口,该接口没有任何抽象方法只是起到一个标记作用。
1.//对象输出流
2.ObjectOutputStream objectOutputStream =
3.new ObjectOutputStream(new FileOutputStream(new File(“D://obj”)));
4.objectOutputStream.writeObject(new User(“zhangsan”, 100));
5.objectOutputStream.close();
76
6.//对象输入流
7.ObjectInputStream objectInputStream =
8.new ObjectInputStream(new FileInputStream(new File(“D://obj”)));
9.User user = (User)objectInputStream.readObject();
10.System.out.println(user);
11.objectInputStream.close();
4.字节流和字符流的区别(2017-11-23-wzz)
字节流读取的时候,读到一个字节就返回一个字节; 字符流使用了字节流读到一个或多个字节(中文对应的字节数是两个,在 UTF-8 码表中是 3 个字节)时。先去查指定的编码表,将查到的字符返回。 字节流可以处理所有类型数据,如:图片,MP3,AVI 视频文件,而字符流只能处理字符数据。只要是处理纯文本数据,就要优先考虑使用字符流,除此之外都用字节流。字节流主要是操作 byte 类型数据,以 byte 数组为准,主要操作类就是 OutputStream、InputStream
字符流处理的单元为 2 个字节的 Unicode 字符,分别操作字符、字符数组或字符串,而字节流处理单元为 1 个字节,操作字节和字节数组。所以字符流是由 Java 虚拟机将字节转化为 2 个字节的 Unicode 字符为单位的字符而成的, 所以它对多国语言支持性比较好!如果是音频文件、图片、歌曲,就用字节流好点,如果是关系到中文(文本)的,用字符流好点。在程序中一个字符等于两个字节,java 提供了 Reader、Writer 两个专门操作字符流的类。
5. 如何实现对象克隆?(2017-11-12-wl)
有两种方式。
1).实现 Cloneable 接口并重写 Object 类中的 clone()方法;
2).实现 Serializable 接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆,代码如下。
12.import java.io.ByteArrayInputStream;
13.import java.io.ByteArrayOutputStream;
14.import java.io.ObjectInputStream;
77
15.import java.io.ObjectOutputStream;
16.import java.io.Serializable;
17.public class MyUtil {
18.private MyUtil() {
19.throw new AssertionError();
20. }
21.@SuppressWarnings(“unchecked”)
22.public static T clone(T obj) throws Exception {
23.ByteArrayOutputStream bout = new ByteArrayOutputStream();
24.ObjectOutputStream oos = new ObjectOutputStream(bout);
25.oos.writeObject(obj);
26.ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
27.ObjectInputStream ois = new ObjectInputStream(bin);
28.return (T) ois.readObject();
29.// 说明:调用 ByteArrayInputStream 或 ByteArrayOutputStream 对象的 close 方法没有任何意义
30.// 这两个基于内存的流只要垃圾回收器清理对象就能够释放资源,这一点不同于对外部资源(如文件流)的释放
31. }
32. }
测试代码:
-
import java.io.Serializable; 2. /**
-
- 人类
-
*/
5.class Person implements Serializable {
6.private static final long serialVersionUID = -9102017020286042305L;
7.private String name; // 姓名
8.private int age; // 年龄
9.private Car car; // 座驾
10.public Person(String name, int age, Car car) {
11.this.name = name;
12.this.age = age;
13.this.car = car; -
}
15.public String getName() {
16.return name; -
}
18.public void setName(String name) {
19.this.name = name; -
}
21.public int getAge() {
22.return age;
78 -
}
24.public void setAge(int age) {
25.this.age = age; -
}
27.public Car getCar() {
28.return car; -
}
30.public void setCar(Car car) {
31.this.car = car; -
}
33.@Override
34.public String toString() {
35.return “Person [name=” + name + “, age=” + age + “, car=” + car + “]”; -
}
-
}
-
/**
-
- 小汽车类
-
*/
4.class Car implements Serializable {
5.private static final long serialVersionUID = -5713945027627603702L;
6.private String brand; // 品牌
7.private int maxSpeed; // 最高时速
8.public Car(String brand, int maxSpeed) {
9.this.brand = brand;
10.this.maxSpeed = maxSpeed; -
}
12.public String getBrand() {
13.return brand; -
}
15.public void setBrand(String brand) {
16.this.brand = brand; -
}
18.public int getMaxSpeed() {
19.return maxSpeed; -
}
21.public void setMaxSpeed(int maxSpeed) {
22.this.maxSpeed = maxSpeed; -
}
24.@Override
25.public String toString() {
26.return “Car [brand=” + brand + “, maxSpeed=” + maxSpeed + “]”;
79 -
}
-
}
1.class CloneTest {
2.public static void main(String[] args) {
3.try {
4.Person p1 = new Person(“Hao LUO”, 33, new Car(“Benz”, 300));
5.Person p2 = MyUtil.clone(p1); // 深度克隆
6.p2.getCar().setBrand(“BYD”);
7.// 修改克隆的 Person 对象 p2 关联的汽车对象的品牌属性
8.// 原来的 Person 对象 p1 关联的汽车不会受到任何影响
9.// 因为在克隆 Person 对象时其关联的汽车对象也被克隆了
10.System.out.println(p1);
11.} catch (Exception e) {
12.e.printStackTrace();
13. }
14. }
15. }
注意:基于序列化和反序列化实现的克隆不仅仅是深度克隆,更重要的是通过泛型限定,可以检查出要克隆的对象是否支持序列化,这项检查是编译器完成的,不是在运行时抛出异常,这种是方案明显优于使用 Object 类的 clone 方法克隆对象。让问题在编译的时候暴露出来总是好过把问题留到运行时。
6. 什么是 java 序列化,如何实现 java 序列化?(2017-12-7-lyq)
序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。序列化是为了解决在对对象流进行读写操作时所引发的问题。
序 列 化 的 实 现 : 将 需 要 被 序 列 化 的 类 实 现 Serializable 接 口 , 该 接 口 没 有 需 要 实 现 的 方 法 , implements Serializable 只是为了标注该对象是可被序列化的,然后使用一个输出流(如:FileOutputStream)来构造一个 ObjectOutputStream(对象流)对象,接着,使用 ObjectOutputStream 对象的 writeObject(Object obj)方法就可以将参数为 obj 的对象写出(即保存其状态),要恢复的话则用输入流。
原文链接:https://www.cnblogs.com/yangchunze/p/6728086.html
八、Java 的集合
1.HashMap 排序题,上机题。(本人主要靠这道题入职的第一家公司)
已知一个 HashMap<Integer,User>集合, User 有 name(String)和 age(int)属性。请写一个方法实现对
HashMap 的排序功能,该方法接收 HashMap<Integer,User>为形参,返回类型为 HashMap<Integer,User>, 要求对 HashMap 中的 User 的 age 倒序进行排序。排序时 key=value 键值对不得拆散。
注意:要做出这道题必须对集合的体系结构非常的熟悉。HashMap 本身就是不可排序的,但是该道题偏偏让给
HashMap 排序,那我们就得想在 API 中有没有这样的 Map 结构是有序的,LinkedHashMap,对的,就是他,他是
Map 结构,也是链表结构,有序的,更可喜的是他是 HashMap 的子类,我们返回 LinkedHashMap<Integer,User>
即可,还符合面向接口(父类编程的思想)。
但凡是对集合的操作,我们应该保持一个原则就是能用 JDK 中的 API 就有 JDK 中的 API,比如排序算法我们不应该去用冒泡或者选择,而是首先想到用 Collections 集合工具类。
82
38.//返回结果
39.return linkedHashMap;
40. }
41. } 42.
2.集合的安全性问题
请问 ArrayList、HashSet、HashMap 是线程安全的吗?如果不是我想要线程安全的集合怎么办?
我们都看过上面那些集合的源码(如果没有那就看看吧),每个方法都没有加锁,显然都是线程不安全的。话又说过来如果他们安全了也就没第二问了。
在集合中 Vector 和 HashTable 倒是线程安全的。你打开源码会发现其实就是把各自核心方法添加上了
synchronized 关键字。
Collections 工具类提供了相关的 API,可以让上面那 3 个不安全的集合变为安全的。
1.// Collections.synchronizedCollection©
2.// Collections.synchronizedList(list)
3.// Collections.synchronizedMap(m)
4.// Collections.synchronizedSet(s)
上面几个函数都有对应的返回值类型,传入什么类型返回什么类型。打开源码其实实现原理非常简单,就是将集合的核心方法添加上了 synchronized 关键字。
3.ArrayList 内部用什么实现的?(2015-11-24)
(回答这样的问题,不要只回答个皮毛,可以再介绍一下 ArrayList 内部是如何实现数组的增加和删除的,因为数组在创建的时候长度是固定的,那么就有个问题我们往 ArrayList 中不断的添加对象,它是如何管理这些数组呢?)
ArrayList 内部是用 Object[]实现的。接下来我们分别分析 ArrayList 的构造、add、remove、clear 方法的实现原理。
一、构造函数
1)空参构造
/**
- Constructs a new {@code ArrayList} instance with zero initial capacity.
*/
public ArrayList() {
array = EmptyArray.OBJECT;
}
array 是一个 Object[]类型。当我们 new 一个空参构造时系统调用了 EmptyArray.OBJECT 属性,EmptyArray 仅仅是一个系统的类库,该类源码如下:
public final class EmptyArray {
private EmptyArray() {}
public static final boolean[] BOOLEAN = new boolean[0]; public static final byte[] BYTE = new byte[0];
public static final char[] CHAR = new char[0]; public static final double[] DOUBLE = new double[0]; public static final int[] INT = new int[0];
public static final Class<?>[] CLASS = new Class[0]; public static final Object[] OBJECT = new Object[0]; public static final String[] STRING = new String[0];
public static final Throwable[] THROWABLE = new Throwable[0];
public static final StackTraceElement[] STACK_TRACE_ELEMENT = new StackTraceElement[0];
}
也就是说当我们 new 一个空参 ArrayList 的时候,系统内部使用了一个 new Object[0]数组。
2)带参构造 1
/**
*Constructs a new instance of {@code ArrayList} with the specified
*initial capacity.
*
*@param capacity
*the initial capacity of this {@code ArrayList}.
*/
public ArrayList(int capacity) { if (capacity < 0) {
throw new IllegalArgumentException("capacity < 0: " + capacity);
}
array = (capacity == 0 ? EmptyArray.OBJECT : new Object[capacity]);
}
该构造函数传入一个 int 值,该值作为数组的长度值。如果该值小于 0,则抛出一个运行时异常。如果等于 0,则使用一个空数组,如果大于 0,则创建一个长度为该值的新数组。
3)带参构造 2
/**
*Constructs a new instance of {@code ArrayList} containing the elements of
*the specified collection.
*
*@param collection
*the collection of elements to add.
*/
public ArrayList(Collection<? extends E> collection) { if (collection == null) {
throw new NullPointerException(“collection == null”);
}
Object[] a = collection.toArray(); if (a.getClass() != Object[].class) {
Object[] newArray = new Object[a.length]; System.arraycopy(a, 0, newArray, 0, a.length); a = newArray;
}
array = a;
size = a.length;
}
如果调用构造函数的时候传入了一个 Collection 的子类,那么先判断该集合是否为 null,为 null 则抛出空指针异常。如果不是则将该集合转换为数组 a,然后将该数组赋值为成员变量 array,将该数组的长度作为成员变量 size。这里面它先判断 a.getClass 是否等于 Object[].class,其实一般都是相等的,我也暂时没想明白为什么多加了这个判断,
toArray 方法是 Collection 接口定义的,因此其所有的子类都有这样的方法,list 集合的 toArray 和 Set 集合的 toArray
返回的都是 Object[]数组。
这里讲些题外话,其实在看 Java 源码的时候,作者的很多意图都很费人心思,我能知道他的目标是啥,但是不知
道他为何这样写。比如对于 ArrayList, array 是他的成员变量,但是每次在方法中使用该成员变量的时候作者都会重新在方法中开辟一个局部变量,然后给局部变量赋值为 array,然后再使用,有人可能说这是为了防止并发修改 array, 毕竟 array 是成员变量,大家都可以使用因此需要将 array 变为局部变量,然后再使用,这样的说法并不是都成立的, 也许有时候就是老外们写代码的一个习惯而已。
二、add 方法
add 方法有两个重载,这里只研究最简单的那个。
/**
*Adds the specified object at the end of this {@code ArrayList}.
*
*@param object
*the object to add.
*@return always true
*/
@Override public boolean add(E object) { Object[] a = array;
int s = size;
if (s == a.length) {
Object[] newArray = new Object[s +
(s < (MIN_CAPACITY_INCREMENT / 2) ? MIN_CAPACITY_INCREMENT : s >> 1)];
System.arraycopy(a, 0, newArray, 0, s); array = a = newArray;
}
a[s] = object; size = s + 1; modCount++; return true;
}
1、首先将成员变量 array 赋值给局部变量 a,将成员变量 size 赋值给局部变量 s。
2、判断集合的长度 s 是否等于数组的长度(如果集合的长度已经等于数组的长度了,说明数组已经满了,该重新分 配 新 数 组 了 ) , 重 新 分 配 数 组 的 时 候 需 要 计 算 新 分 配 内 存 的 空 间 大 小 , 如 果 当 前 的 长 度 小 于
MIN_CAPACITY_INCREMENT/2(这个常量值是 12,除以 2 就是 6,也就是如果当前集合长度小于 6)则分配 12 个
长度,如果集合长度大于 6 则分配当前长度 s 的一半长度。这里面用到了三元运算符和位运算,s >> 1,意思就是将
s 往右移 1 位,相当于 s=s/2,只不过位运算是效率最高的运算。3、将新添加的 object 对象作为数组的 a[s]个元素。
4、修改集合长度 size 为 s+1
5、modCotun++,该变量是父类中声明的,用于记录集合修改的次数,记录集合修改的次数是为了防止在用迭代器迭代集合时避免并发修改异常,或者说用于判断是否出现并发修改异常的。
6、return true,这个返回值意义不大,因为一直返回 true,除非报了一个运行时异常。三、remove 方法
remove 方法有两个重载,我们只研究 remove(int index)方法。
/**
*Removes the object at the specified location from this list.
*
*@param index
*the index of the object to remove.
*@return the removed object.
*@throws IndexOutOfBoundsException
*when {@code location < 0 || location >= size()}
*/
@Override public E remove(int index) { Object[] a = array;
int s = size;
if (index >= s) { throwIndexOutOfBoundsException(index, s);
}
@SuppressWarnings(“unchecked”) E result = (E) a[index];
System.arraycopy(a, index + 1, a, index, --s - index); a[s] = null; // Prevent memory leak
size = s; modCount++; return result;
}
1、先将成员变量 array 和 size 赋值给局部变量 a 和 s。
87
2、判断形参 index 是否大于等于集合的长度,如果成了则抛出运行时异常
3、获取数组中脚标为 index 的对象 result,该对象作为方法的返回值
4、调用 System 的 arraycopy 函数,拷贝原理如下图所示。
5、接下来就是很重要的一个工作,因为删除了一个元素,而且集合整体向前移动了一位,因此需要将集合最后一个元素设置为 null,否则就可能内存泄露。
6、重新给成员变量 array 和 size 赋值
7、记录修改次数
8、返回删除的元素(让用户再看最后一眼) 四、clear 方法
如果集合长度不等于 0,则将所有数组的值都设置为 null,然后将成员变量 size 设置为 0 即可,最后让修改记录加 1。
4.并发集合和普通集合如何区别?(2015-11-24)
并发集合常见的有 ConcurrentHashMap、ConcurrentLinkedQueue、ConcurrentLinkedDeque 等。并发集合位 于 java.util.concurrent 包 下 , 是 jdk1.5 之 后 才 有 的 , 主 要 作 者 是 Doug Lea
(http://baike.baidu.com/view/3141057.htm)完成的。
在 java 中有普通集合、同步(线程安全)的集合、并发集合。普通集合通常性能最高,但是不保证多线程的安全性和并发的可靠性。线程安全集合仅仅是给集合添加了 synchronized 同步锁,严重牺牲了性能,而且对并发的效率就更低了,并发集合则通过复杂的策略不仅保证了多线程的安全又提高的并发时的效率。
参考阅读:
ConcurrentHashMap 是线程安全的 HashMap 的实现,默认构造同样有 initialCapacity 和 loadFactor 属性, 不过还多了一个 concurrencyLevel 属性,三属性默认值分别为 16、0.75 及 16。其内部使用锁分段技术,维持这锁
Segment 的数组,在 Segment 数组中又存放着 Entity[]数组,内部 hash 算法将数据较均匀分布在不同锁中。
put 操作:并没有在此方法上加上 synchronized,首先对 key.hashcode 进行 hash 操作,得到 key 的 hash 值。
hash 操作的算法和map 也不同,根据此hash 值计算并获取其对应的数组中的Segment 对象(继承自ReentrantLock), 接着调用此 Segment 对象的 put 方法来完成当前操作。
ConcurrentHashMap 基于 concurrencyLevel 划分出了多个 Segment 来对 key-value 进行存储,从而避免每次 put 操作都得锁住整个数组。在默认的情况下,最佳情况下可允许 16 个线程并发无阻塞的操作集合对象,尽可能地减少并发时的阻塞现象。
get(key)
首先对 key.hashCode 进行 hash 操作,基于其值找到对应的 Segment 对象,调用其 get 方法完成当前操作。而 Segment 的 get 操作首先通过 hash 值和对象数组大小减 1 的值进行按位与操作来获取数组上对应位置的
HashEntry。在这个步骤中,可能会因为对象数组大小的改变,以及数组上对应位置的 HashEntry 产生不一致性,那么 ConcurrentHashMap 是如何保证的?
对象数组大小的改变只有在 put 操作时有可能发生,由于 HashEntry 对象数组对应的变量是 volatile 类型的,因此可以保证如 HashEntry 对象数组大小发生改变,读操作可看到最新的对象数组大小。
在获取到了 HashEntry 对象后,怎么能保证它及其 next 属性构成的链表上的对象不会改变呢?这点
ConcurrentHashMap 采用了一个简单的方式,即 HashEntry 对象中的 hash、key、next 属性都是 final 的,这也就意味着没办法插入一个 HashEntry 对象到基于 next 属性构成的链表中间或末尾。这样就可以保证当获取到 HashEntry 对象后,其基于 next 属性构建的链表是不会发生变化的。
ConcurrentHashMap 默认情况下采用将数据分为 16 个段进行存储,并且 16 个段分别持有各自不同的锁Segment,锁仅用于 put 和 remove 等改变集合对象的操作,基于 volatile 及 HashEntry 链表的不变性实现了读取的不加锁。这些方式使得 ConcurrentHashMap 能够保持极好的并发支持,尤其是对于读远比插入和删除频繁的 Map 而言,而它采用的这些方法也可谓是对于 Java 内存模型、并发机制深刻掌握的体现。
推荐博客地址:http://m.oschina.net/blog/269037
5.List 的三个子类的特点(2017-2-23)
ArrayList 底层结构是数组,底层查询快,增删慢。
LinkedList 底层结构是链表型的,增删快,查询慢。
voctor 底层结构是数组 线程安全的,增删慢,查询慢。
6.List 和 Map、Set 的区别(2017-11-22-wzz)
6.1结构特点
List 和 Set 是存储单列数据的集合,Map 是存储键和值这样的双列数据的集合;List 中存储的数据是有顺序,并且允许重复;Map 中存储的数据是没有顺序的,其键是不能重复的,它的值是可以有重复的,Set 中存储的数据是无序的,且不允许有重复,但元素在集合中的位置由元素的 hashcode 决定,位置是固定的(Set 集合根据 hashcode 来
91
进行数据的存储,所以位置是固定的,但是位置不是用户可以控制的,所以对于用户来说 set 中的元素还是无序的);
6.2实现类
List 接口有三个实现类(LinkedList:基于链表实现,链表内存是散乱的,每一个元素存储本身内存地址的同时还存储下一个元素的地址。链表增删快,查找慢;ArrayList:基于数组实现,非线程安全的,效率高,便于索引,但不便于插入删除;Vector:基于数组实现,线程安全的,效率低)。
Map 接口有三个实现类(HashMap:基于 hash 表的 Map 接口实现,非线程安全,高效,支持 null 值和 null 键;HashTable:线程安全,低效,不支持 null 值和 null 键;LinkedHashMap:是 HashMap 的一个子类,保存了记录的插入顺序;SortMap 接口:TreeMap,能够把它保存的记录根据键排序,默认是键值的升序排序)。
Set 接口有两个实现类(HashSet:底层是由 HashMap 实现,不允许集合中有重复的值,使用该方式时需要重写 equals()和 hashCode()方法;LinkedHashSet:继承与 HashSet,同时又基于 LinkedHashMap 来进行实现,底层使用的是 LinkedHashMp)。
6.3区别
List 集合中对象按照索引位置排序,可以有重复对象,允许按照对象在集合中的索引位置检索对象,例如通过
list.get(i)方法来获取集合中的元素;Map 中的每一个元素包含一个键和一个值,成对出现,键对象不可以重复,值对象可以重复;Set 集合中的对象不按照特定的方式排序,并且没有重复对象,但它的实现类能对集合中的对象按照特定的方式排序,例如 TreeSet 类,可以按照默认顺序,也可以通过实现 Java.util.Comparator接口来自定义排序方式。
7.HashMap 和 HashTable 有什么区别?(2017-2-23)
HashMap 是线程不安全的,HashMap 是一个接口,是 Map 的一个子接口,是将键映射到值得对象,不允许键值重复,
允许空键和空值;由于非线程安全,HashMap 的效率要较 HashTable 的效率高一些.
HashTable 是线程安全的一个集合,不允许 null 值作为一个 key 值或者 Value 值;
HashTable 是 sychronize,多个线程访问时不需要自己为它的方法实现同步,而 HashMap 在被多个线程访问的时候需要自己为它的方法实现同步;
8.数组和链表分别比较适合用于什么场景,为什么?(2017-2-23)
8.1数组和链表简介
在计算机中要对给定的数据集进行若干处理,首要任务是把数据集的一部分(当数据量非常大时,可能只能一部分一部分地读取数据到内存中来处理)或全部存储到内存中,然后再对内存中的数据进行各种处理。
例如,对于数据集 S{1,2,3,4,5,6},要求 S 中元素的和,首先要把数据存储到内存中,然后再将内存中的数据相加。
当内存空间中有足够大的连续空间时,可以把数据连续的存放在内存中,各种编程语言中的数组一般都是按这种方式存储的(也可能有例外),如图 1(b);当内存中只有一些离散的可用空间时,想连续存储数据就非常困难了, 这时能想到的一种解决方式是移动内存中的数据,把离散的空间聚集成连续的一块大空间,如图 1(c)所示,这样做当然也可以,但是这种情况因为可能要移动别人的数据,所以会存在一些困难,移动的过程中也有可能会把一些别人的重要数据给丢失。另外一种,不影响别人的数据存储方式是把数据集中的数据分开离散地存储到这些不连续空间中, 如图(d)。这时为了能把数据集中的所有数据联系起来,需要在前一块数据的存储空间中记录下一块数据的地址,这样只要知道第一块内存空间的地址就能环环相扣地把数据集整体联系在一起了。C/C++中用指针实现的链表就是这种 存储形式。
图 内 存 分 配
由上可知,内存中的存储形式可以分为连续存储和离散存储两种。因此,数据的物理存储结构就有连续存储和离散存储两种,它们对应了我们通常所说的数组和链表,
8.2数组和链表的区别
数组是将元素在内存中连续存储的;它的优点:因为数据是连续存储的,内存地址连续,所以在查找数据的时候效率比较高;它的缺点:在存储之前,我们需要申请一块连续的内存空间,并且在编译的时候就必须确定好它的空间的大小。在运行的时候空间的大小是无法随着你的需要进行增加和减少而改变的,当数据两比较大的时候,有可能会出现越界的情况,数据比较小的时候,又有可能会浪费掉内存空间。在改变数据个数时,增加、插入、删除数据效率比较低链表是动态申请内存空间,不需要像数组需要提前申请好内存的大小,链表只需在用的时候申请就可以,根据需
要来动态申请或者删除内存空间,对于数据增加和删除以及插入比数组灵活。还有就是链表中数据在内存中可以在任意的位置,通过应用来关联数据(就是通过存在元素的指针来联系)
8.3链表和数组使用场景
数组应用场景:数据比较少;经常做的运算是按序号访问数据元素;数组更容易实现,任何高级语言都支持;构建的线性表较稳定。
链表应用场景:对线性表的长度或者规模难以估计;频繁做插入删除操作;构建动态性比较强的线性表。参考博客:http://blog.csdn.net/u011277123/article/details/53908387
8.4跟数组相关的面试题
用面向对象的方法求出数组中重复 value 的个数,按如下个数输出:
1 出现:1 次
3 出现:2 次
8 出现:3 次
2 出现:4 次
int[] arr = {1,4,1,4,2,5,4,5,8,7,8,77,88,5,4,9,6,2,4,1,5};
9.Java 中 ArrayList 和 Linkedlist 区别?(2017-2-23)
ArrayList 和 Vector 使用了数组的实现,可以认为 ArrayList 或者 Vector 封装了对内部数组的操作,比如向数组中添加,删除,插入新的元素或者数据的扩展和重定向。
LinkedList 使用了循环双向链表数据结构。与基于数组的 ArrayList 相比,这是两种截然不同的实现技术,这也决定了它们将适用于完全不同的工作场景。
LinkedList 链表由一系列表项连接而成。一个表项总是包含 3 个部分:元素内容,前驱表和后驱表,如图所示:
在下图展示了一个包含 3 个元素的 LinkedList 的各个表项间的连接关系。在 JDK 的实现中,无论 LikedList 是否为空,链表内部都有一个 header 表项,它既表示链表的开始,也表示链表的结尾。表项 header 的后驱表项便是链表中第一个元素,表项 header 的前驱表项便是链表中最后一个元素。
10.List a=new ArrayList()和 ArrayList a =new ArrayList()的区别?(2017-2-
24)
List list = new ArrayList();这句创建了一个 ArrayList 的对象后把上溯到了 List。此时它是一个 List 对象了,有些
ArrayList 有但是 List 没有的属性和方法,它就不能再用了。而 ArrayList list=new ArrayList();创建一对象则保留了
ArrayList 的所有属性。 所以需要用到 ArrayList 独有的方法的时候不能用前者。实例代码如下:
1.List list = new ArrayList();
2.ArrayList arrayList = new ArrayList(); 3.list.trimToSize(); //错误,没有该方法。
4.arrayList.trimToSize(); //ArrayList 里有该方法。
- 要对集合更新操作时,ArrayList 和 LinkedList 哪个更适合?(2017-2-24)
1.ArrayList 是实现了基于动态数组的数据结构,LinkedList 基于链表的数据结构。
2.如果集合数据是对于集合随机访问 get 和 set,ArrayList 绝对优于 LinkedList,因为 LinkedList 要移动指针。
3.如果集合数据是对于集合新增和删除操作 add 和 remove,LinedList 比较占优势,因为 ArrayList 要移动数
据。
ArrayList 和 LinkedList 是两个集合类,用于存储一系列的对象引用(references)。例如我们可以用 ArrayList 来存储一系列的 String 或者 Integer。那 么 ArrayList 和 LinkedList 在性能上有什么差别呢?什么时候应该用 ArrayList 什么时候又该用 LinkedList 呢?
一.时间复杂度
首先一点关键的是,ArrayList 的内部实现是基于基础的对象数组的,因此,它使用 get 方法访问列表中的任意一个元素时(random access),它的速度要比 LinkedList 快。LinkedList 中的 get 方法是按照顺序从列表的一端开始检查,直到另外一端。对 LinkedList 而言,访问列表中的某个指定元素没有更快的方法了。
假设我们有一个很大的列表,它里面的元素已经排好序了,这个列表可能是 ArrayList 类型的也可能是 LinkedList 类型的,现在我们对这个列表来进行二分查找(binary search),比较列表是 ArrayList 和 LinkedList 时的查询速度, 看下面的程序:
得到的输出是:
1.ArrayList 消耗时间:15
2.LinkedList 消耗时间:2596
这个结果不是固定的,但是基本上 ArrayList 的时间要明显小于 LinkedList 的时间。因此在这种情况下不宜用
LinkedList。二分查找法使用的随机访问(random access)策略,而 LinkedList 是不支持快速的随机访问的。对一个
98
LinkedList 做随机访问所消耗的时间与这个 list 的大小是成比例的。而相应的,在 ArrayList 中进行随机访问所消耗的时间是固定的。
这是否表明 ArrayList 总是比 LinkedList 性能要好呢?这并不一定,在某些情况下 LinkedList 的表现要优于
ArrayList,有些算法在 LinkedList 中实现 时效率更高。比方说,利用 Collections.reverse 方法对列表进行反转时, 其性能就要好些。看这样一个例子,加入我们有一个列表,要对其进行大量的插入和删除操作,在这种情况下 LinkedList 就是一个较好的选择。请看如下一个极端的例子,我们重复的在一个列表的开端插入一个元素:
1.import java.util.*;
2.public class ListDemo {
3. static final int N=50000;
4. static long timeList(List list){
5. long start=System.currentTimeMillis(); 6. Object o = new Object();
7. for(int i=0;i<N;i++)
8. list.add(0, o);
9. return System.currentTimeMillis()-start; 10. }
11. public static void main(String[] args) {
12. System.out.println(“ArrayList 耗时:”+timeList(new ArrayList())); 13. System.out.println(“LinkedList 耗时:”+timeList(new LinkedList())); 14. }
15.}
这时我的输出结果是
1.ArrayList 耗时:2463
2.LinkedList 耗时:15
二.空间复杂度
在 LinkedList 中有一个私有的内部类,定义如下:
1.private static class Entry {
2. Object element;
3. Entry next;
4. Entry previous;
5. }
每个 Entry 对象 reference 列表 中的一个元素,同时还有在 LinkedList 中它的上一个元素和下一个元素。一个有 1000 个元素的 LinkedList 对象将有 1000 个链接在一起 的 Entry 对象,每个对象都对应于列表中的一个元素。这样的话,在一个 LinkedList 结构中将有一个很大的空间开销,因为它要存储这 1000 个 Entity 对象的相关信息。
ArrayList 使用一个内置的数组来存 储元素,这个数组的起始容量是 10.当数组需要增长时,新的容量按如下公式获得:新容量=(旧容量*3)/2+1,也就是说每一次容量大概会增长 50%。 这就意味着,如果你有一个包含大量元素的 ArrayList 对象,那么最终将有很大的空间会被浪费掉,这个浪费是由 ArrayList 的工作方式本身造成 的。如果没有足够的空间来存放新的元素,数组将不得不被重新进行分配以便能够增加新的元素。对数组进行重新分配,将会导致性能急剧下降。如果我们知道一个 ArrayList 将会有多少个元素,我们可以通过构造方法来指定容量。我们还可以通过 trimToSize 方法在 ArrayList 分配完毕之后去掉浪 费掉的空间。
三.总结
ArrayList 和 LinkedList 在性能上各有优缺点,都有各自所适用的地方,总的说来可以描述如下:
1.对 ArrayList 和 LinkedList 而言,在列表末尾增加一个元素所花的开销都是固定的。对 ArrayList 而言,主要是在内部数组中增加一项,指向所添加的元素,偶 尔可能会导致对数组重新进行分配;而对 LinkedList 而言,这个开销是统一的,分配一个内部 Entry 对象。
2.在 ArrayList 的中间插入或删除一个元素意味着这个列表中剩余的元素都会被移动;而在 LinkedList 的中间插入或删除一个元素的开销是固定的。
3.LinkedList 不支持高效的随机元素访问。
4.ArrayList 的空间浪费主要体现在在 list 列表的结尾预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗相当的空间
可以这样说:当操作是在一列数据的后面添加数据而不是在前面或中间,并且需要随机地访问其中的元素时,使用
ArrayList 会提供比较好的性能;当你的操作是在一列数据的前面或中间添加或删除数据,并且按照顺序访问其中的元素时,就应该使用 LinkedList 了。
12.请用两个队列模拟堆栈结构(2017-2-24)
两个队列模拟一个堆栈,队列是先进先出,而堆栈是先进后出。模拟如下队列 a 和 b
(1)入栈:a 队列为空,b 为空。例:则将”a,b,c,d,e”需要入栈的元素先放 a 中,a 进栈为”a,b,c,d,e”
(2)出栈:a 队列目前的元素为”a,b,c,d,e”。将 a 队列依次加入 Arraylist 集合 a 中。以倒序的方法,将 a 中的集合取出,放入 b 队列中,再将 b 队列出列。代码如下:
1. public static void main(String[] args) {
2. Queue queue = new LinkedList(); //a 队 列
3. Queue queue2=new LinkedList(); //b 队列
4. ArrayList a=new ArrayList(); //arrylist 集合是中间参数
5. //往 a 队列添加元素
6. queue.offer(“a”);
7. queue.offer(“b”);
8. queue.offer(“c”);
9. queue.offer(“d”);
10. queue.offer(“e”);
11. System.out.print(“进栈:”); 12. //a 队列依次加入 list 集合之中13. for(String q : queue){
14. a.add(q);
15. System.out.print(q); 16. }
17. //以倒序的方法取出(a 队列依次加入 list 集合)之中的值,加入 b 对列
18. for(int i=a.size()-1;i>=0;i–){ 19. queue2.offer(a.get(i));
20. }
21. //打印出栈队列
22. System.out.println("");
23. System.out.print(“出栈:”);
24. for(String q : queue2){
25. System.out.print(q); 26. }
27. }
打印结果为(遵循栈模式先进后出):
进栈:a b c d e
出栈:e d c b a
13.Collection 和 Map 的集成体系(2017-11-14-lyq)
Collection:
Map:
14.Map 中的 key 和 value 可以为 null 么?(2017-11-21-gxb)
HashMap 对象的 key、value 值均可为 null。HahTable 对象的 key、value 值均不可为 null。
且两者的的 key 值均不能重复,若添加 key 相同的键值对,后面的 value 会自动覆盖前面的 value,但不会报错。测试代码如下:
- public class Test {
3.public static void main(String[] args) {
4.Map<String, String> map = new HashMap<String, String>();//HashMap 对象
5.Map<String, String> tableMap = new Hashtable<String, String>();//HashTable 对象
6.
7.map.put(null, null);
8.System.out.println(“hashMap 的[key]和[value]均可以为 null:” + map.get(null)); 9.
10.try {
11.tableMap.put(null, “3”);
12.System.out.println(tableMap.get(null));
13.} catch (Exception e) {
14.System.out.println("【ERROR】:hashTable 的[key]不能为 null");
15. }
16.
17.try {
18.tableMap.put(“3”, null);
103
19.System.out.println(tableMap.get(“3”));
20.} catch (Exception e) {
21.System.out.println("【ERROR】:hashTable 的[value]不能为 null");
22. }
23. } 24.
25. }
运行结果:
hashMap 的[key]和[value]均可以为 null:null
【ERROR】:hashTable 的[key]不能为 null
【ERROR】:hashTable 的[value]不能为 null
九、Java 的多线程和并发库
对于 Java 程序员来说,多线程在工作中的使用场景还是比较常见的,而仅仅掌握了 Java 中的传统多线程机制, 还是不够的。在 JDK5.0 之后,Java 增加的并发库中提供了很多优秀的 API,在实际开发中用的比较多。因此在看具体的面试题之前我们有必要对这部分知识做一个全面的了解。
(一)多线程基础知识–传统线程机制的回顾(2017-12-11-wl)
( 1 ) 传统使用类 Thread 和接口 Runnable 实现
1.在 Thread 子类覆盖的 run 方法中编写运行代码方式一
new Thread(){ @Override
public void run(){ while(true){
try { Thread.sleep(2000);
} catch (InterruptedException e) { e.printStackTrace();
}
}
104
}
}.start();
2.在传递给 Thread 对象的 Runnable 对象的 run 方法中编写代码
new Thread(new Runnable(){
public void run(){ while(true){
try { Thread.sleep(2000);
} catch (InterruptedException e) { e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
}
}).start();
3.总结
查看 Thread 类的 run()方法的源代码,可以看到其实这两种方式都是在调用 Thread 对象的 run 方法,如果 Thread
类的 run 方法没有被覆盖,并且为该 Thread 对象设置了一个 Runnable 对象,该 run 方法会调用 Runnable 对象的
run 方法
/**
*If this thread was constructed using a separate
*Runnable
run object, then that
*Runnable
object’s run
method is called;
*otherwise, this method does nothing and returns.
*Subclasses of Thread
should override this method.
*
*@see #start()
*@see #stop()
*@see #Thread(ThreadGroup, Runnable, String)
*/ @Override
public void run() {
if (target != null) { target.run();
}
105
}
( 2 ) 定实现时器 Timer 和 TimerTask
Timer 在实际开发中应用场景不多,一般来说都会用其他第三方库来实现。但有时会在一些面试题中出现。下面我们就针对一道面试题来使用 Timer 定时类。
1.请模拟写出双重定时器(面试题)
要求:使用定时器,间隔 4 秒执行一次,再间隔 2 秒执行一次,以此类推执行。
class TimerTastCus extends TimerTask{ @Override
public void run() { count = (count +1)%2;
System.err.println("Boob boom ");
new Timer().schedule(new TimerTastCus(), 2000+2000*count);
}
}
Timer timer = new Timer();
timer.schedule(new TimerTastCus(), 2000+2000*count);
while (true) {
System.out.println(new Date().getSeconds()); try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block e.printStackTrace();
}
}
//PS:下面的代码中的 count 变量中
//此参数要使用在你匿名内部类中,使用 final 修饰就无法对其值进行修改,
//只能改为静态变量
private static volatile int count = 0;
( 3 ) 线程互斥与同步
在引入多线程后,由于线程执行的异步性,会给系统造成混乱,特别是在急用临界资源时,如多个线程急用同一台打印机,会使打印结果交织在一起,难于区分。当多个线程急用共享变量,表格,链表时,可能会导致数据处理出错,
106
因此线程同步的主要任务是使并发执行的各线程之间能够有效的共享资源和相互合作,从而使程序的执行具有可再现性。 当线程并发执行时,由于资源共享和线程协作,使用线程之间会存在以下两种制约关系。
1.间接相互制约。一个系统中的多个线程必然要共享某种系统资源,如共享 CPU,共享 I/O 设备,所谓间接相互制约即源于这种资源共享,打印机就是最好的例子,线程 A 在使用打印机时,其它线程都要等待。
2.直接相互制约。这种制约主要是因为线程之间的合作,如有线程 A 将计算结果提供给线程 B 作进一步处理, 那么线程 B 在线程 A 将数据送达之前都将处于阻塞状态。
间接相互制约可以称为互斥,直接相互制约可以称为同步,对于互斥可以这样理解,线程 A 和线程 B 互斥访问某个资源则它们之间就会产个顺序问题——要么线程 A 等待线程 B 操作完毕,要么线程 B 等待线程操作完毕,这其实就是线程的同步了。因此同步包括互斥,互斥其实是一种特殊的同步。
下面我们通过一道面试题来体会线程的交互。
要求:子线程运行执行 10 次后,主线程再运行 5 次。这样交替执行三遍
public static void main(String[] args) {
final Bussiness bussiness = new Bussiness();
//子线程
new Thread(new Runnable() { @Override
public void run() {
for (int i = 0; i < 3; i++) { bussiness.subMethod();
}
}
}).start();
//主线程
for (int i = 0; i < 3; i++) { bussiness.mainMethod();
}
}
}
class Bussiness {
107
private boolean subFlag = true;
public synchronized void mainMethod() { while (subFlag) {
try { wait();
} catch (InterruptedException e) { e.printStackTrace();
}
}
for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName()
- " : main thread running loop count – " + i);
try { Thread.sleep(1000);
} catch (InterruptedException e) { e.printStackTrace();
}
}
subFlag = true; notify();
}
public synchronized void subMethod() { while (!subFlag) {
try { wait();
} catch (InterruptedException e) { e.printStackTrace();
}
}
for (int i = 0; i < 10; i++) { System.err.println(Thread.currentThread().getName()
- " : sub thread running loop count – " + i);
try { Thread.sleep(1000);
} catch (InterruptedException e) { e.printStackTrace();
}
}
subFlag = false;
108
notify();
}
}
( 4 ) 线程局部变量 ThreadLocal
ThreadLocal 的作用和目的:用于实现线程内的数据共享,即对于相同的程序代码,多个模块在同一个线程中运行时要共享一份数据,而在另外线程中运行时又共享另外一份数据。
每个线程调用全局 ThreadLocal 对象的 set 方法,在 set 方法中,首先根据当前线程获取当前线程的
ThreadLocalMap 对象,然后往这个 map 中插入一条记录,key 其实是 ThreadLocal 对象,value 是各自的 set 方法传进去的值。也就是每个线程其实都有一份自己独享的ThreadLocalMap 对象,该对象的Key 是ThreadLocal 对象,值是用户设置的具体值。在线程结束时可以调用 ThreadLocal.remove()方法,这样会更快释放内存,不调用也可以,因为线程结束后也可以自动释放相关的 ThreadLocal 变量。
ThreadLocal 的应用场景:
订单处理包含一系列操作:减少库存量、增加一条流水台账、修改总账,这几个操作要在同一个事务中完成,通常也即同一个线程中进行处理,如果累加公司应收款的操作失败了,则应该把前面的操作回滚,否则,提交所有操作,这要求这些操作使用相同的数据库连接对象,而这些操作的代码分别位于不同的模块类中。
银行转账包含一系列操作: 把转出帐户的余额减少,把转入帐户的余额增加,这两个操作要在同一个事务中完成,它们必须使用相同的数据库连接对象,转入和转出操作的代码分别是两个不同的帐户对象的方法。
例如 Strut2 的 ActionContext,同一段代码被不同的线程调用运行时,该代码操作的数据是每个线程各自的状态和数据,对于不同的线程来说,getContext 方法拿到的对象都不相同,对同一个线程来说,不管调用 getContext 方法多少次和在哪个模块中 getContext 方法,拿到的都是同一个。
1.ThreadLocal 的使用方式
(1)在关联数据类中创建 private static ThreadLocal
在下面的类中,私有静态 ThreadLocal 实例(serialNum)为调用该类的静态 SerialNum.get() 方法的每个线程维护了一个“序列号”,该方法将返回当前线程的序列号。(线程的序列号是在第一次调用 SerialNum.get() 时分配的,并在后续调用中不会更改。)
public class SerialNum {
// The next serial number to be assigned private static int nextSerialNum = 0;
private static ThreadLocal serialNum = new ThreadLocal() { protected synchronized Object initialValue() {
return new Integer(nextSerialNum++);
}
};
public static int get() {
return ((Integer) (serialNum.get())).intValue();
}
}
另一个例子,也是私有静态 ThreadLocal 实例:
public class ThreadContext {
private String userId;
private Long transactionId;
private static ThreadLocal threadLocal = new ThreadLocal(){
@Override
protected ThreadContext initialValue() {
return new ThreadContext();
}
};
public static ThreadContext get() {
return threadLocal.get();
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) { this.userId = userId;
}
public Long getTransactionId() { return transactionId;
}
public void setTransactionId(Long transactionId) { this.transactionId = transactionId;
}
}
补充:在 JDK 的 API 对 ThreadLocal 私有化的说明。并举例‘线程唯一标识符’
UniqueThreadIdGenerator ,大家学习是可以结合官方 API 来学习。
2.在 Util 类中创建 ThreadLocal
这是上面用法的扩展,即把 ThreadLocal 的创建放到工具类中。
public class HibernateUtil {
private static Log log = LogFactory.getLog(HibernateUtil.class);
private static final SessionFactory sessionFactory; //定义 SessionFactory
static {
try {
// 通过默认配置文件 hibernate.cfg.xml 创建 SessionFactory
sessionFactory = new Configuration().configure().buildSessionFactory();
} catch (Throwable ex) {
log.error(“初始化 SessionFactory 失败!”, ex);
throw new ExceptionInInitializerError(ex);
}
}
//创建线程局部变量 session,用来保存 Hibernate 的 Session
public static final ThreadLocal session = new ThreadLocal();
/**
- 获取当前线程中的 Session
- @return Session
- @throws HibernateException
*/
public static Session currentSession() throws HibernateException {
Session s = (Session) session.get();
// 如果 Session 还没有打开,则新开一个 Session
if (s == null) {
s = sessionFactory.openSession();
session.set(s); //将新开的 Session 保存到线程局部变量中
}
return s;
}
public static void closeSession() throws HibernateException {
//获取线程局部变量,并强制转换为 Session 类型
Session s = (Session) session.get();
session.set(null);
if (s != null)
s.close();
}
}
3.在 Runnable 中创建 ThreadLocal
在线程类内部创建 ThreadLocal,基本步骤如下:
①、在多线程的类(如 ThreadDemo 类)中,创建一个 ThreadLocal 对象 threadXxx,用来保存线程间需要隔离处理的对象 xxx。
②、在 ThreadDemo 类中,创建一个获取要隔离访问的数据的方法 getXxx(),在方法中判断,若
ThreadLocal 对象为 null 时候,应该 new()一个隔离访问类型的对象,并强制转换为要应用的类型
③、在 ThreadDemo 类的 run()方法中,通过调用 getXxx()方法获取要操作的数据,这样可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象。
Random random = new Random();
int age = random.nextInt(100);
System.out.println(currentThreadName + " is set age: " + age);
Studen studen = getStudent (); //通过这个方法,为每个线程都独立的 new 一个 student 对象,每个线程的的
student 对象都可以设置不同的值
studen.setAge(age);
System.out.println(currentThreadName + " is first get age: " + studen.getAge());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println( currentThreadName + " is second get age: " + studen.getAge());
}
private Studen getStudent () {
Studen studen = studenThreadLocal.get();
if (null == studen) {
studen = new Studen();
studenThreadLocal.set(studen);
}
return studen;
}
public static void main(String[] args) {
ThreadLocalTest t = new ThreadLocalTest();
Thread t1 = new Thread(t,"Thread A");
Thread t2 = new Thread(t,"Thread B");
t1.start();
t2.start();
}
}
class Studen{
int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
( 5 ) 多线程共享数据
在 Java 传统线程机制中的共享数据方式,大致可以简单分两种情况:
多个线程行为一致,共同操作一个数据源。也就是每个线程执行的代码相同,可以使用同一个 Runnable 对象,这个 Runnable 对象中有那个共享数据,例如,卖票系统就可以这么做。
多个线程行为不一致,共同操作一个数据源。也就是每个线程执行的代码不同,这时候需要用不同的
Runnable 对象。例如,银行存取款。
下面我们通过两个示例代码来分别说明这两种方式。
- 多个线程行为一致共同操作一个数据
如果每个线程执行的代码相同,可以使用同一个 Runnable 对象,这个 Runnable 对象中有那个共享数据,例如, 买票系统就可以这么做。
/**
*共享数据类
/
class ShareData{
private int num = 10 ;
public synchronized void inc() { num++;
System.out.println(Thread.currentThread().getName()+": invoke inc method num =" + num); try {
Thread.sleep(1000);
} catch (InterruptedException e) { e.printStackTrace();
}
}
}
/
*多线程类
**/ class RunnableCusToInc implements Runnable{
private ShareData shareData;
114
public RunnableCusToInc(ShareData data) { this.shareData = data;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) { shareData.inc();
}
}
}
/**
*测试方法
**/
public static void main(String[] args) { ShareData shareData = new ShareData();
for (int i = 0; i < 4; i++) {
new Thread(new RunnableCusToInc(shareData),"Thread "+ i).start();
}
}
}
- 多个线程行为不一致共同操作一个数据
如果每个线程执行的代码不同,这时候需要用不同的 Runnable 对象,有如下两种方式来实现这些 Runnable 对象之间的数据共享:
1)将共享数据封装在另外一个对象中,然后将这个对象逐一传递给各个 Runnable 对象。每个线程对共享数据的操作方法也分配到那个对象身上去完成,这样容易实现针对该数据进行的各个操作的互斥和通信。
115
}
}
//封装共享数据类
class RunnableCusToInc implements Runnable{
// 封 装 共 享 数 据 private ShareData shareData;
public RunnableCusToInc(ShareData data) { this.shareData = data;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) { shareData.inc();
}
}
}
//封装共享数据类
class RunnableCusToDec implements Runnable{
// 封 装 共 享 数 据 private ShareData shareData;
public RunnableCusToDec(ShareData data) { this.shareData = data;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) { shareData.dec();
}
}
}
/**
*共享数据类
**/
class ShareData{
private int num = 10 ;
public synchronized void inc() { num++;
System.out.println(Thread.currentThread().getName()+": invoke inc method num =" + num);
116
try { Thread.sleep(1000);
} catch (InterruptedException e) { e.printStackTrace();
}
}
}
2)将这些 Runnable 对象作为某一个类中的内部类,共享数据作为这个外部类中的成员变量,每个线程对共享数据的操作方法也分配给外部类,以便实现对共享数据进行的各个操作的互斥和通信,作为内部类的各个
Runnable 对象调用外部类的这些方法。
public static void main(String[] args) {
//公共数据
final ShareData shareData = new ShareData(); for (int i = 0; i < 4; i++) {
if(i%2 == 0){
new Thread(new Runnable() { @Override
public void run() {
for (int i = 0; i < 5; i++) { shareData.inc();
}
}
},"Thread "+ i).start();
}else{
new Thread(new Runnable() { @Override
public void run() {
for (int i = 0; i < 5; i++) { shareData.dec();
}
}
},"Thread "+ i).start();
}
}
}
class ShareData{
private int num = 10 ;
public synchronized void inc() { num++;
System.out.println(Thread.currentThread().getName()+": invoke inc method num =" + num); try {
Thread.sleep(1000);
} catch (InterruptedException e) { e.printStackTrace();
}
}
public synchronized void dec() { num–;
System.err.println(Thread.currentThread().getName()+": invoke dec method num =" + num); try {
Thread.sleep(1000);
} catch (InterruptedException e) { e.printStackTrace();
}
}
}
补充:上面两种方式的组合:将共享数据封装在另外一个对象中,每个线程对共享数据的操作方法也分配到那个对象身上去完成,对象作为这个外部类中的成员变量或方法中的局部变量,每个线程的 Runnable 对象作为外部类中的成员内部类或局部内部类。
总之,要同步互斥的几段代码最好是分别放在几个独立的方法中,这些方法再放在同一个类中,这样比较容易实现它们之间的同步互斥和通信。
(二)多线程基础知识–线程并发库(2017-12-11-wl)
Java 5 添加了一个新的包到 Java 平台,java.util.concurrent 包。这个包包含有一系列能够让 Java 的并发编程变得更加简单轻松的类。在这个包被添加以前,你需要自己去动手实现自己的相关工具类。下面带你认识下
java.util.concurrent 包里的这些类,然后你可以尝试着如何在项目中使用它们。本文中将使用 Java 6 版本,我不确
定这和 Java 5 版本里的是否有一些差异。我不会去解释关于 Java 并发的核心问题 – 其背后的原理,也就是说, 如果你对那些东西感兴趣,参考《Java 并发指南》。
( 1 ) Java 的线程并发库介绍
Java5 的多线程并有两个大发库在 java.util.concurrent 包及子包中,子包主要的包有一下两个
1)java.util.concurrent 包 (多线程并发库)
java.util.concurrent 包含许多线程安全、测试良好、高性能的并发构建块。不客气地说, 创建
java.util.concurrent 的目的就是要实现 Collection 框架对数据结构所执行的并发操作。通过提供一组可靠的、高性能并发构建块,开发人员可以提高并发类的线程安全、可伸缩性、性能、可读性和可靠性,后面、我们会做介绍。
如果一些类名看起来相似,可能是因为 java.util.concurrent 中的许多概念源自 Doug Lea 的
util.concurrent 库 。
2)java.util.concurrent.atomic 包 (多线程的原子性操作提供的工具类)
查看 atomic 包文档页下面的介绍,它可以对多线程的基本数据、数组中的基本数据和对象中的基本数据进行多线程的操作(AtomicInteger、AtomicIntegerArray、AtomicIntegerFieldUpDater…)
通过如下两个方法快速理解 atomic 包的意义:
AtomicInteger 类 的 boolean compareAndSet(expectedValue, updateValue);
AtomicIntegerArray 类 的 int addAndGet(int i, int delta);
顺带解释 volatile 类型的作用,需要查看 java 语言规范。
volatile 修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最的值。(具有可见性)
volatile 没有原子性。
3)java.util.concurrent.lock 包 (多线程的锁机制)
119
为锁和等待条件提供一个框架的接口和类,它不同于内置同步和监视器。该框架允许更灵活地使用锁和条件。本包下有三大接口,下面简单介绍下:
Lock 接口:支持那些语义不同(重入、公平等)的锁规则,可以在非阻塞式结构的上下文(包括 hand- over-hand 和锁重排算法)中使用这些规则。主要的实现是 ReentrantLock。
ReadWriteLock 接口:以类似方式定义了一些读取者可以共享而写入者独占的锁。此包只提供了一个实现,即 ReentrantReadWriteLock,因为它适用于大部分的标准用法上下文。但程序员可以创建自己的、适用于非标准要求的实现。
Condition 接口:描述了可能会与锁有关联的条件变量。这些变量在用法上与使用 Object.wait 访问的隐式监视器类似,但提供了更强大的功能。需要特别指出的是,单个 Lock 可能与多个 Condition 对象关联。为了避免兼容性问题,Condition 方法的名称与对应的 Object 版本中的不同。
( 2 ) Java 的并发库入门
下面我们将分别介绍 java.util.concurrent 包下的常用类的使用。
1)java.util.concurrent 包
java.util.concurrent 包描述:
在并发编程中很常用的实用工具类。此包包括了几个小的、已标准化的可扩展框架,以及一些提供有用功能的类。此包下有一些组件,其中包括:
执行程序(线程池)
并发队列
同步器
并发 Collocation
下面我们将 java.util.concurrent 包下的组件逐一简单介绍:
A. 执行程序
Executors 线程池工厂类
首次我们来说下线程池的作用:
线程池作用就是限制系统中执行线程的数量。
根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果;少了浪费了系统资源,多了造成系统拥挤效率不高。用线程池控制线程数量,其他线程 排队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程 池中有等待的工作线程,就可以开始运行了;否则进入等待队列。
为什么要用线程池:
减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务
可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为因为消耗过多的内存,而把服务器累趴下(每个线程需要大约 1MB 内存,线程开的越多,消耗的内存也就越大,最后死机)
Executors 详解:
Java 里面线程池的顶级接口是 Executor,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是 ExecutorService。ThreadPoolExecutor 是 Executors 类的底层实现。我们先介绍下 Executors。
线程池的基本思想还是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线
程执行调度由池管理器来处理。当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源。
Java5 中并发库中,线程池创建线程大致可以分为下面三种:
//创建固定大小的线程池
ExecutorService fPool = Executors.newFixedThreadPool(3);
//创建缓存大小的线程池
ExecutorService cPool = Executors.newCachedThreadPool();
//创建单一的线程池
ExecutorService sPool = Executors.newSingleThreadExecutor();
下面我们通过简单示例来分别说明:
固定大小连接池
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
/**
- Java 线程:线程池-
- @author Administrator 2009-11-4 23:30:44
*/
public class Test {
public static void main(String[] args) {
//创建一个可重用固定线程数的线程池
ExecutorService pool = Executors.newFixedThreadPool(2);
//创建实现了 Runnable 接口对象,Thread 对象当然也实现了 Runnable 接口
Thread t1 = new MyThread();
Thread t2 = new MyThread();
Thread t3 = new MyThread();
Thread t4 = new MyThread();
Thread t5 = new MyThread();
//将线程放入池中进行执行
pool.execute(t1);
pool.execute(t2);
pool.execute(t3);
pool.execute(t4);
pool.execute(t5);
//关闭线程池
pool.shutdown();
}
122
}
class MyThread extends Thread{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+“正在执行。。。”);
}
}
运行结果:
pool-1-thread-1 正在执行。。。
pool-1-thread-1 正在执行。。。
pool-1-thread-2 正在执行。。。
pool-1-thread-1 正在执行。。。
pool-1-thread-2 正在执行。。。
从上面的运行来看,我们 Thread 类都是在线程池中运行的,线程池在执行 execute 方法来执行 Thread 类中的 run 方法。不管 execute 执行几次,线程池始终都会使用 2 个线程来处理。不会再去创建出其他线程来处理
run 方法执行。这就是固定大小线程池。
单任务连接池
我们将上面的代码
//创建一个可重用固定线程数的线程池
ExecutorService pool = Executors.newFixedThreadPool(2);
改为:
//创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。
ExecutorService pool = Executors.newSingleThreadExecutor();
运行结果:
pool-1-thread-1 正在执行。。。
pool-1-thread-1 正在执行。。。
pool-1-thread-1 正在执行。。。
pool-1-thread-1 正在执行。。。
pool-1-thread-1 正在执行。。。
运行结果看出,单任务线程池在执行 execute 方法来执行 Thread 类中的 run 方法。不管 execute 执行几次,线程池始终都会使用单个线程来处理。
补充:在 java 的多线程中,一但线程关闭,就会成为死线程。关闭后死线程就没有办法在启动了。再次启动就会出现
123
异常信息:Exception in thread “main” java.lang.IllegalThreadStateException。那么如何解决这个问题呢? 我们这里就可以使用 Executors.newSingleThreadExecutor()来再次启动一个线程。(面试)
可变连接池
//创建一个可重用固定线程数的线程池
ExecutorService pool = Executors.newFixedThreadPool(2);
改为:
//创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。
ExecutorService pool = Executors.newCachedThreadPool();
运行结果:
pool-1-thread-5 正在执行。。。
pool-1-thread-1 正在执行。。。
pool-1-thread-4 正在执行。。。
pool-1-thread-3 正在执行。。。
pool-1-thread-2 正在执行。。。
运行结果看出,可变任务线程池在执行 execute 方法来执行 Thread 类中的 run 方法。这里 execute 执行多次, 线程池就会创建出多个线程来处理 Thread 类中 run 方法。所有我们看到连接池会根据执行的情况,在程序运行时创建多个线程来处理,这里就是可变连接池的特点。
那么在上面的三种创建方式,Executors 还可以在执行某个线程时,定时操作。那么下面我们通过代码简单演示下。
延迟连接池
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
- Java 线程:线程池-
- @author Administrator 2009-11-4 23:30:44
*/
public class Test {
public static void main(String[] args) {
124
//创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
//创建实现了 Runnable 接口对象,Thread 对象当然也实现了 Runnable 接口
Thread t1 = new MyThread();
Thread t2 = new MyThread();
Thread t3 = new MyThread();
Thread t4 = new MyThread();
Thread t5 = new MyThread();
//将线程放入池中进行执行
pool.execute(t1);
pool.execute(t2);
pool.execute(t3);
//使用定时执行风格的方法
pool.schedule(t4, 10, TimeUnit.MILLISECONDS); //t4 和 t5 在 10 秒后执行
pool.schedule(t5, 10, TimeUnit.MILLISECONDS);
//关闭线程池
pool.shutdown();
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + “正在执行。。。”);
}
}
运行结果:
pool-1-thread-1 正在执行。。。
pool-1-thread-2 正在执行。。。
pool-1-thread-1 正在执行。。。
pool-1-thread-1 正在执行。。。
pool-1-thread-2 正在执行。。。
ExecutorService 执行器服务
java.util.concurrent.ExecutorService 接口表示一个异步执行机制,使我们能够在后台执行任务。因此一个 ExecutorService 很类似于一个线程池。实际上,存在于 java.util.concurrent 包里的 ExecutorService 实现就是一个线程池实现。
ExecutorService 例子:
125
以下是一个简单的 ExecutorService 例子:
//线程工厂类创建出线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
//执行一个线程任务executorService.execute(new Runnable() {
public void run() { System.out.println(“Asynchronous task”);
}
});
//线程池关闭executorService.shutdown();
上面代码首先使用 newFixedThreadPool() 工厂方法创建一个 ExecutorService。这里创建了一个十个线程执行任务的线程池。然后,将一个 Runnable 接口的匿名实现类传递给 execute() 方法。这将导致 ExecutorService 中的某个线程执行该 Runnable。这里可以看成一个任务分派,示例代码中的任务分派我们可以理解为:
一个线程将一个任务委派给一个 ExecutorService 去异步执行。
一旦该线程将任务委派给 ExecutorService,该线程将继续它自己的执行,独立于该任务的执行。
如下图:
Thread1
http://w 程序员
Task
ExecutorService
ExecutorService 实现:
既然 ExecutorService 是个接口,如果你想用它的话就得去使用它的实现类之一。
java.util.concurrent 包提供了 ExecutorService 接口的以下实现类:
ThreadPoolExecutor
ScheduledThreadPoolExecutor
ExecutorService 创建:
ExecutorService 的创建依赖于你使用的具体实现。但是你也可以使用 Executors 工厂类来创建
ExecutorService 实例。代码示例:
ExecutorService executorService1 = Executors.newSingleThreadExecutor(); //之前 Executors 已介绍
ExecutorService executorService2 = Executors.newFixedThreadPool(10);
ExecutorService executorService3 = Executors.newScheduledThreadPool(10);
ExecutorService 使用:
有几种不同的方式来将任务委托给 ExecutorService 去执行:
execute(Runnable)
submit(Runnable)
127
submit(Callable)
invokeAny(…)
invokeAll(…)
接下来我们挨个看一下这些方法。
execute(Runnable) execute(Runnable) 方法要求一个 java.lang.Runnable 对象, 然后对它进行异步执行。以下是使用ExecutorService 执行一个 Runnable 的示例:
特点:没有办法得知被执行的 Runnable 的执行结果。如果有需要的话你得使用一个 Callable(以下将做介绍)。
submit(Runnable)
submit(Runnable) 方法也要求一个 Runnable 实现类,但它返回一个 Future 对象。这个 Future 对象可以用来检查 Runnable 是否已经执行完毕。以下是 ExecutorService submit() 示例:
//从 Executors 中获得 ExecutorService
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future future = executorService.submit(new Runnable() { public void run() {
System.out.println(“Asynchronous task”);
}
128
});
future.get(); //获得执行完 run 方法后的返回值,这里使用的 Runnable,所以这里没有返回值,返回的是 null。executorService.shutdown();
submit(Runnable)
submit(Callable) 方法类似于 submit(Runnable) 方法,除了它所要求的参数类型之外。Callable 实例除了它的 call() 方法能够返回一个结果之外和一个 Runnable 很相像。Runnable.run() 不能够返回一个结果。Callable 的结果可以通过 submit(Callable) 方法返回的 Future 对象进行获取。
以下是一个 ExecutorService Callable 示例:
//从 Executors 中获得 ExecutorService
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future future = executorService.submit(new Callable(){ public Object call() throws Exception {
System.out.println(“Asynchronous Callable”); return “Callable Result”;
}
});
System.out.println("future.get() = " + future.get()); executorService.shutdown();
输出:
Asynchronous Callable
future.get() = Callable Result
invokeAny()
invokeAny() 方法要求一系列的 Callable 或者其子接口的实例对象。调用这个方法并不会返回一个 Future, 但它返回其中一个 Callable 对象的结果。无法保证返回的是哪个 Callable 的结果 – 只能表明其中一个已 执行结束。
如果其中一个任务执行结束(或者抛了一个异常),其他 Callable 将被取消。以下是示例代码:
ExecutorService executorService = Executors.newSingleThreadExecutor();
129
Set<Callable> callables = new HashSet<Callable>();
callables.add(new Callable() { public String call() throws Exception {
return “Task 1”;
}
});
callables.add(new Callable() { public String call() throws Exception {
return “Task 2”;
}
});
callables.add(new Callable() { public String call() throws Exception {
return “Task 3”;
}
});
String result = executorService.invokeAny(callables);
System.out.println("result = " + result);
executorService.shutdown();
上述代码将会打印出给定 Callable 集合中的一个的执行结果。我自己试着执行了它几次,结果始终在变。有时是 “Task 1″,有时是 “Task 2″ 等等。
invokeAll()
invokeAll() 方法将调用你在集合中传给 ExecutorService 的所有 Callable 对象。invokeAll() 返回一系列的 Future 对象,通过它们你可以获取每个 Callable 的执行结果。
记住,一个任务可能会由于一个异常而结束,因此它可能没有 “成功”。无法通过一个 Future 对象来告知我们是两种结束中的哪一种。
以下是一个代码示例:
ExecutorService executorService = Executors.newSingleThreadExecutor();
Set<Callable> callables = new HashSet<Callable>();
callables.add(new Callable() { public String call() throws Exception {
return “Task 1”;
}
});
callables.add(new Callable() { public String call() throws Exception {
return “Task 2”;
}
});
callables.add(new Callable() { public String call() throws Exception {
return “Task 3”;
}
});
List<Future> futures = executorService.invokeAll(callables);
for(Future future : futures){ System.out.println("future.get = " + future.get());
}
executorService.shutdown();
输出结果:
future.get = Task 3 future.get = Task 1
future.get = Task 2
Executors 关闭:
使用 shutdown 和 shutdownNow 可以关闭线程池两者的区别:
shutdown 只是将空闲的线程 interrupt() 了,shutdown()之前提交的任务可以继续执行直到结束。
shutdownNow 是 interrupt 所有线程, 因此大部分线程将立刻被中断。之所以是大部分,而不是全部 , 是因为 interrupt()方法能力有限。
ThreadPoolExecutor 线程池执行者
java.util.concurrent.ThreadPoolExecutor 是 ExecutorService 接口的一个实现。ThreadPoolExecutor 使用其内部池中的线程执行给定任务(Callable 或者 Runnable)。
ThreadPoolExecutor 包含的线程池能够包含不同数量的线程。池中线程的数量由以下变量决定:
corePoolSize
maximumPoolSize
当一个任务委托给线程池时,如果池中线程数量低于 corePoolSize,一个新的线程将被创建,即使池中可能尚有空 闲 线 程 。 如 果 内 部 任 务 队 列 已 满 , 而 且 有 至 少 corePoolSize 正 在 运 行 , 但 是 运 行 线 程 的 数 量 低 于
maximumPoolSize,一个新的线程将被创建去执行该任务。
ThreadPoolExecutor 图解:
Thread Pool Executor
maxPoolSize
corePoolSize
创建 ThreadPoolExecutor:
132
int maxPoolSize = 10; long keepAliveTime = 5000;
ExecutorService threadPoolExecutor = new ThreadPoolExecutor(
corePoolSize, maxPoolSize, keepAliveTime, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()
);
构造方法参数列表解释:
corePoolSize - 池中所保存的线程数,包括空闲线程。
maximumPoolSize - 池中允许的最大线程数。
keepAliveTime - 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。
unit - keepAliveTime 参数的时间单位。
workQueue - 执行前用于保持任务的队列。此队列仅保持由 execute 方法提交的 Runnable 任务。
ScheduledPoolExecutor 定时线程池执行者
java.util.concurrent.ScheduledExecutorService 是一个 ExecutorService, 它能够将任务延后执行,或者间隔固定时间多次执行。 任务由一个工作者线程异步执行,而不是由提交任务给 ScheduledExecutorService 的那个线程执行。
ScheduledPoolExecutor 例子:
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
ScheduledFuture scheduledFuture = scheduledExecutorService.schedule(new Callable() {
public Object call() throws Exception { System.out.println(“Executed!”); return “Called!”;
}
}, 5,
TimeUnit.SECONDS);//5 秒后执行
133
首先一个内置 5 个线程的 ScheduledExecutorService 被创建。之后一个 Callable 接口的匿名类示例被创建然后传递给 schedule() 方法。后边的俩参数定义了 Callable 将在 5 秒钟之后被执行。
ScheduledExecutorService 的实现:
ScheduledExecutorService 是一个接口,你要用它的话就得使用 java.util.concurrent 包里对它的某个实现类。
ScheduledExecutorService 具有以下实现类:ScheduledThreadPoolExecutor
创建一个 ScheduledExecutorService:
如何创建一个 ScheduledExecutorService 取决于你采用的它的实现类。但是你也可以使用 Executors 工厂类来创建一个 ScheduledExecutorService 实例。比如:
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
ScheduledExecutorService 的使用:
一旦你创建了一个 ScheduledExecutorService,你可以通过调用它的以下方法:
schedule (Callable task, long delay, TimeUnit timeunit)
schedule (Runnable task, long delay, TimeUnit timeunit)
scheduleAtFixedRate (Runnable, long initialDelay, long period, TimeUnit timeunit)
scheduleWithFixedDelay (Runnable, long initialDelay, long period, TimeUnit timeunit)
下面我们就简单看一下这些方法。
schedule (Callable task, long delay, TimeUnit timeunit)
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
ScheduledFuture scheduledFuture = scheduledExecutorService.schedule(new Callable() {
134
public Object call() throws Exception { System.out.println(“Executed!”); return “Called!”;
}
}, 5,
TimeUnit.SECONDS);
System.out.println("result = " + scheduledFuture.get());
scheduledExecutorService.shutdown();
输出结果:
Executed!
result = Called!
schedule (Runnable task, long delay, TimeUnit timeunit)
这一方法规划一个任务将被定期执行。该任务将会在首个 initialDelay 之后得到执行,然后每个 period 时间之后重复执行。
如果给定任务的执行抛出了异常,该任务将不再执行。如果没有任何异常的话,这个任务将会持续循环执行到
ScheduledExecutorService 被关闭。
如果一个任务占用了比计划的时间间隔更长的时候,下一次执行将在当前执行结束执行才开始。计划任务在同一时间不会有多个线程同时执行。
scheduleAtFixedRate (Runnable, long initialDelay, long period, TimeUnit timeunit)
这一方法规划一个任务将被定期执行。该任务将会在首个 initialDelay 之后得到执行,然后每个 period 时间之后重复执行。
如果给定任务的执行抛出了异常,该任务将不再执行。如果没有任何异常的话,这个任务将会持续循环执行到
ScheduledExecutorService 被关闭。
如果一个任务占用了比计划的时间间隔更长的时候,下一次执行将在当前执行结束执行才开始。计划任务在同一
时间不会有多个线程同时执行。
135
scheduleWithFixedDelay (Runnable, long initialDelay, long period, TimeUnit timeunit)
除了 period 有不同的解释之外这个方法和 scheduleAtFixedRate() 非常像。scheduleAtFixedRate() 方法中,
period 被解释为前一个执行的开始和下一个执行的开始之间的间隔时间。而在本方法中,period 则被解释为前一个执行的结束和下一个执行的结束之间的间隔。因此这个延迟是执行结束之间的间隔,而不是执行开始之间的间隔。
ScheduledExecutorService 的关闭:
正如 ExecutorService,在你使用结束之后你需要把 ScheduledExecutorService 关闭掉。否则他将导致 JVM
继续运行,即使所有其他线程已经全被关闭。
你 可 以 使 用 从 ExecutorService 接 口 继 承 来 的 shutdown() 或 shutdownNow() 方 法 将
ScheduledExecutorService 关闭。参见 ExecutorService 关闭部分以获取更多信息。
ForkJoinPool 合并和分叉(线程池)
ForkJoinPool 在 Java 7 中被引入。它和 ExecutorService 很相似,除了一点不同。ForkJoinPool 让我们可以很方便地把任务分裂成几个更小的任务,这些分裂出来的任务也将会提交给 ForkJoinPool。任务可以继续分割成更小的子任务,只要它还能分割。可能听起来有些抽象,因此本节中我们将会解释 ForkJoinPool 是如何工作的,还有任务分割是如何进行的。
合并和分叉的解释:
在我们开始看 ForkJoinPool 之前我们先来简要解释一下分叉和合并的原理。分叉和合并原理包含两个递归进行的步骤。两个步骤分别是分叉步骤和合并步骤。
分叉:
一个使用了分叉和合并原理的任务可以将自己分叉(分割)为更小的子任务,这些子任务可以被并发执行。如下图所
示:
Task
http://www.ithei
Fork
Task Task
程序员
通过把自己分割成多个子任务,每个子任务可以由不同的 CPU 并行执行,或者被同一个 CPU 上的不同线程执行。只有当给的任务过大,把它分割成几个子任务才有意义。把任务分割成子任务有一定开销,因此对于小型任务,这个分割的消耗可能比每个子任务并发执行的消耗还要大。
什么时候把一个任务分割成子任务是有意义的,这个界限也称作一个阀值。这要看每个任务对有意义阀值的决定。很大程度上取决于它要做的工作的种类。
合并:
当一个任务将自己分割成若干子任务之后,该任务将进入等待所有子任务的结束之中。一旦子任务执行结束,该任务可以把所有结果合并到同一个结果。图示如下:
当然,并非所有类型的任务都会返回一个结果。如果这个任务并不返回一个结果,它只需等待所有子任务执行完毕。也就不需要结果的合并啦。
所以我们可以将 ForkJoinPool 是一个特殊的线程池,它的设计是为了更好的配合 分叉-和-合并 任务分割的工作。ForkJoinPool 也在 java.util.concurrent 包中,其完整类名为 java.util.concurrent.ForkJoinPool。
创建一个 ForkJoinPool:
你可以通过其构造子创建一个 ForkJoinPool。作为传递给 ForkJoinPool 构造子的一个参数,你可以定义你期望的并行级别。并行级别表示你想要传递给 ForkJoinPool 的任务所需的线程或 CPU 数量。以下是一个 ForkJoinPool 示例:
提交任务到 ForkJoinPool:
就像提交任务到 ExecutorService 那样,把任务提交到 ForkJoinPool。你可以提交两种类型的任务。一种是没有任何返回值的(一个 “行动”),另一种是有返回值的(一个”任务”)。这两种类型分别由 RecursiveAction 和
RecursiveTask 表示。接下来介绍如何使用这两种类型的任务,以及如何对它们进行提交。
RecursiveAction:
RecursiveAction 是一种没有任何返回值的任务。它只是做一些工作,比如写数据到磁盘,然后就退出了。一个
RecursiveAction 可以把自己的工作分割成更小的几块,这样它们可以由独立的线程或者 CPU 执行。你可以通过继承来实现一个 RecursiveAction。示例如下:
138
@Override
protected void compute() {
//if work is above threshold, break tasks up into smaller tasks
//翻译:如果工作超过门槛,把任务分解成更小的任务if(this.workLoad > 16) {
System.out.println("Splitting workLoad : " + this.workLoad);
List subtasks =
new ArrayList();
subtasks.addAll(createSubtasks());
for(RecursiveAction subtask : subtasks){ subtask.fork();
}
} else {
System.out.println("Doing workLoad myself: " + this.workLoad);
}
}
private List createSubtasks() { List subtasks =
new ArrayList();
MyRecursiveAction subtask1 = new MyRecursiveAction(this.workLoad / 2); MyRecursiveAction subtask2 = new MyRecursiveAction(this.workLoad / 2);
subtasks.add(subtask1); subtasks.add(subtask2);
return subtasks;
}
}
例子很简单。MyRecursiveAction 将一个虚构的 workLoad 作为参数传给自己的构造子。如果 workLoad 高于一个特定阀值,该工作将被分割为几个子工作,子工作继续分割。如果 workLoad 低于特定阀值,该工作将由
MyRecursiveAction 自己执行。你可以这样规划一个 MyRecursiveAction 的执行:
139
//创建了一个并行级别为 4 的 ForkJoinPool
ForkJoinPool forkJoinPool = new ForkJoinPool(4);
//创建一个没有返回值的任务
MyRecursiveAction myRecursiveAction = new MyRecursiveAction(24);
//ForkJoinPool 执行任务
forkJoinPool.invoke(myRecursiveAction);
运行结果:
Splitting workLoad : 24
Doing workLoad myself: 12
RecursiveTask:
RecursiveTask 是一种会返回结果的任务。它可以将自己的工作分割为若干更小任务,并将这些子任务的执行结果合并到一个集体结果。可以有几个水平的分割和合并。以下是一个 RecursiveTask 示例:
140
}
long result = 0;
for(MyRecursiveTask subtask : subtasks) { result += subtask.join();
}
return result;
} else {
System.out.println("Doing workLoad myself: " + this.workLoad); return workLoad * 3;
}
}
private List createSubtasks() { List subtasks =
new ArrayList();
MyRecursiveTask subtask1 = new MyRecursiveTask(this.workLoad / 2); MyRecursiveTask subtask2 = new MyRecursiveTask(this.workLoad / 2);
subtasks.add(subtask1); subtasks.add(subtask2);
return subtasks;
}
}
除了 有一个结果 返回之外 , 这个示例 和 RecursiveAction 的例子很像 。 MyRecursiveTask 类继 承自
RecursiveTask,这也就意味着它将返回一个 Long 类型的结果。
MyRecursiveTask 示例也会将工作分割为子任务,并通过 fork() 方法对这些子任务计划执行。此外,本示例还通过调用每个子任务的 join() 方法收集它们返回的结果。子任务的结果随后被合并到一个更大的结果,并最终将其返回。对于不同级别的递归,这种子任务的结果合并可能会发生递归。
你可以这样规划一个 RecursiveTask:
//创建了一个并行级别为 4 的 ForkJoinPool
ForkJoinPool forkJoinPool = new ForkJoinPool(4);
//创建一个有返回值的任务
MyRecursiveTask myRecursiveTask = new MyRecursiveTask(128);
141
//线程池执行并返回结果
long mergedResult = forkJoinPool.invoke(myRecursiveTask);
System.out.println("mergedResult = " + mergedResult);
注意: ForkJoinPool.invoke() 方法的调用来获取最终执行结果的。
B.并发队列-阻塞队列
常用的并发队列有阻塞队列和非阻塞队列,前者使用锁实现,后者则使用 CAS 非阻塞算法实现。
PS:至于非阻塞队列是靠 CAS 非阻塞算法,在这里不再介绍,大家只用知道,Java 非阻塞队列是使用 CAS 算法来实现的就可以。感兴趣的童鞋可以维基网上自行学习.
下面我们先介绍阻塞队列。
阻塞队列:
阻塞队列 (BlockingQueue)是 Java util.concurrent 包下重要的数据结构,BlockingQueue 提供了线程安全的队列访问方式:当阻塞队列进行插入数据时,如果队列已满,线程将会阻塞等待直到队列非满;从阻塞队列取数据时,如果队列已空,线程将会阻塞等待直到队列非空。并发包下很多高级同步类的实现都是基于 BlockingQueue 实现的。
BlockingQueue 阻塞队列
BlockingQueue 通常用于一个线程生产对象,而另外一个线程消费这些对象的场景。下图是对这个原理的阐述:
BlockingQueue
Take
一个线程往里边放,另外一个线程从里边取的一个 BlockingQueue。
一个线程将会持续生产新对象并将其插入到队列之中,直到队列达到它所能容纳的临界点。也就是说,它是有限的。如果该阻塞队列到达了其临界点,负责生产的线程将会在往里边插入新对象时发生阻塞。它会一直处于阻塞之中, 直到负责消费的线程从队列中拿走一个对象。负责消费的线程将会一直从该阻塞队列中拿出对象。如果消费线程尝试去从一个空的队列中提取对象的话,这个消费线程将会处于阻塞之中,直到一个生产线程把一个对象丢进队列。
BlockingQueue 的方法:
BlockingQueue 具有 4 组不同的方法用于插入、移除以及对队列中的元素进行检查。如果请求的操作不能得到立即执行的话,每个方法的表现也不同。这些方法如下:
阻塞队列提供了四种处理方法:
方法\处理方式 抛出异常 返回特殊值 一直阻塞 超时退出
插入方法
add(e)
offer(e)
put(e)
offer(e,time,unit)
移除方法
remove()
poll()
take()
poll(time,unit)
检查方法
element()
peek() 不可用 不可用
四组不同的行为方式解释:
抛异常:如果试图的操作无法立即执行,抛一个异常。
特定值:如果试图的操作无法立即执行,返回一个特定的值(常常是 true / false)。
阻塞:如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行。
超时:如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。返回一个特定值以告知该操作是否成功(典型的是 true / false)。
无法向一个 BlockingQueue 中插入 null。如果你试图插入 null, BlockingQueue 将会抛出一个
NullPointerException.
BlockingQueue 的实现类:
BlockingQueue 是个接口,你需要使用它的实现之一来使用 BlockingQueue,Java.util.concurrent 包下具有以下 BlockingQueue 接口的实现类:
ArrayBlockingQueue:ArrayBlockingQueue 是一个有界的阻塞队列,其内部实现是将对象放到一个数组里。有界也就意味着,它不能够存储无限多数量的元素。它有一个同一时间能够存储元素数量的上限。你可以在对其初始化的时候设定这个上限,但之后就无法对这个上限进行修改了(译者注:因为它是基于数组实现的,也就具有数组的特性:一旦初始化,大小就无法修改)。
DelayQueue:DelayQueue 对元素进行持有直到一个特定的延迟到期。注入其中的元素必须实现
java.util.concurrent.Delayed 接口。
LinkedBlockingQueue:LinkedBlockingQueue 内部以一个链式结构(链接节点)对其元素进行存储。如果需要的话,这一链式结构可以选择一个上限。如果没有定义上限,将使用 Integer.MAX_VALUE 作为上限。
PriorityBlockingQueue : PriorityBlockingQueue 是 一 个 无 界 的 并 发 队 列 。 它 使 用 了 和 类
java.util.PriorityQueue 一 样 的 排 序 规 则 。 你 无 法 向 这 个 队 列 中 插 入 null 值 。 所 有 插 入 到
PriorityBlockingQueue 的元素必须实现 java.lang.Comparable 接口。因此该队列中元素的排序就取决于你自己的 Comparable 实现。
SynchronousQueue:SynchronousQueue 是一个特殊的队列,它的内部同时只能够容纳单个元素。如果该队列已有一元素的话,试图向队列中插入一个新元素的线程将会阻塞,直到另一个线程将该元素从队列中抽走。同样,如果该队列为空,试图向队列中抽取一个元素的线程将会阻塞,直到另一个线程向队列中插入了一条新的元素。据此,把这个类称作一个队列显然是夸大其词了。它更多像是一个汇合点。
ArrayBlockingQueue 阻塞队列
ArrayBlockingQueue 类图
如上图 ArrayBlockingQueue 内部有个数组 items 用来存放队列元素,putindex 下标标示入队元素下标,
takeIndex 是出队下标,count 统计队列元素个数,从定义可知道并没有使用 volatile 修饰,这是因为访问这些变量使
145
用都是在锁块内,并不存在可见性问题。另外有个独占锁 lock 用来对出入队操作加锁,这导致同时只有一个线程可以访问入队出队,另外 notEmpty,notFull 条件变量用来进行出入队的同步。
另外构造函数必须传入队列大小参数,所以为有界队列,默认是 Lock 为非公平锁。
public ArrayBlockingQueue(int capacity) { this(capacity, false);
}
public ArrayBlockingQueue(int capacity, boolean fair) { if (capacity <= 0)
throw new IllegalArgumentException(); this.items = new Object[capacity];
lock = new ReentrantLock(fair); notEmpty = lock.newCondition(); notFull = lock.newCondition();
}
ps:
所谓公平锁:就是在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照 FIFO 的规则从队列中取到自己。
非公平锁:比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式
ArrayBlockingQueue 方法
offer 方法
在队尾插入元素,如果队列满则返回 false,否者入队返回 true。
public boolean offer(E e) {
//e 为 null,则抛出 NullPointerException 异常
checkNotNull(e);
//获取独占锁
final ReentrantLock lock = this.lock; lock.lock();
try {
146
//如果队列满则返回 false
if (count == items.length) return false;
else {
//否者插入元素insert(e);
return true;
}
} finally {
//释放锁lock.unlock();
}
}
private void insert(E x) {
//元素入队items[putIndex] = x;
//计算下一个元素应该存放的下标putIndex = inc(putIndex);
++count; notEmpty.signal();
}
//循环队列,计算下标 final int inc(int i) {
return (++i == items.length) ? 0 : i;
}
这里由于在操作共享变量前加了锁,所以不存在内存不可见问题,加过锁后获取的共享变量都是从主内存获取的, 而不是在 CPU 缓存或者寄存器里面的值,释放锁后修改的共享变量值会刷新会主内存中。
另外这个队列是使用循环数组实现, 所以计算下一个元素存放下标时候有些特殊。另外 insert 后调用
notEmpty.signal();是为了激活调用 notEmpty.await()阻塞后放入 notEmpty 条件队列中的线程。
Put 操作
在队列尾部添加元素,如果队列满则等待队列有空位置插入后返回。
public void put(E e) throws InterruptedException {
147
checkNotNull(e);
final ReentrantLock lock = this.lock;
//获取可被中断锁lock.lockInterruptibly();
try {
//如果队列满,则把当前线程放入 notFull 管理的条件队列
while (count == items.length) notFull.await();
//插入元素insert(e);
} finally {
lock.unlock();
}
}
需要注意的是如果队列满了那么当前线程会阻塞,知道出队操作调用了 notFull.signal 方法激活该线程。代码逻辑很简单,但是这里需要思考一个问题为啥调用 lockInterruptibly 方法而不是 Lock 方法。我的理解是因为调用了条件变量的 await()方法,而 await()方法会在中断标志设置后抛出 InterruptedException 异常后退出,所以还不如在加锁时候先看中断标志是不是被设置了,如果设置了直接抛出 InterruptedException 异常,就不用再去获取锁了。然后看了其他并发类里面凡是调用了 await 的方法获取锁时候都是使用的 lockInterruptibly 方法而不是 Lock 也验证了这个想法。
Poll 操作
从队头获取并移除元素,队列为空,则返回 null。
public E poll() {
final ReentrantLock lock = this.lock; lock.lock();
try {
//当前队列为空则返回 null,否者
return (count == 0) ? null : extract();
} finally {
lock.unlock();
}
}
148
private E extract() {
final Object[] items = this.items;
//获取元素值
E x = this.cast(items[takeIndex]);
// 数 组 中 值 值 为 null; items[takeIndex] = null;
//队头指针计算,队列元素个数减一takeIndex = inc(takeIndex);
–count;
//发送信号激活 notFull 条件队列里面的线程
notFull.signal(); return x;
}
Take 操作
从队头获取元素,如果队列为空则阻塞直到队列有元素。
public E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly();
try {
//队列为空,则等待,直到队列有元素while (count == 0)
notEmpty.await(); return extract();
} finally {
lock.unlock();
}
}
需要注意的是如果队列为空,当前线程会被挂起放到 notEmpty 的条件队列里面,直到入队操作执行调用
notEmpty.signal 后当前线程才会被激活,await 才会返回。
Peek 操作
返回队列头元素但不移除该元素,队列为空,返回 null。
149
public E peek() {
final ReentrantLock lock = this.lock; lock.lock();
try {
//队列为空返回 null,否者返回头元素
return (count == 0) ? null : itemAt(takeIndex);
} finally {
lock.unlock();
}
}
final E itemAt(int i) {
return this.cast(items[i]);
}
Size 操作
获取队列元素个数,非常精确因为计算 size 时候加了独占锁,其他线程不能入队或者出队或者删除元素。
public int size() {
final ReentrantLock lock = this.lock; lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
ArrayBlockingQueue 小结
ArrayBlockingQueue 通过使用全局独占锁实现同时只能有一个线程进行入队或者出队操作,这个锁的粒度比较大,有点类似在方法上添加 synchronized 的意味。其中 offer,poll 操作通过简单的加锁进行入队出队操作,而 put,take 则使用了条件变量实现如果队列满则等待,如果队列空则等待,然后分别在出队和入队操作中发送信号激活等待线程实现同步。另外相比 LinkedBlockingQueue,ArrayBlockingQueue 的 size 操作的结果是精确的,因为计算前加了全局锁。
ArrayBlockingQueue 示例
需求:在多线程操作下,一个数组中最多只能存入 3 个元素。多放入不可以存入数组,或等待某线程对数组中某
150
个元素取走才能放入,要求使用 java 的多线程来实现。(面试) 代码实现:
}
}.start();
}
}
输出结果:
Thread-0 准备放数据!
Thread-0 已经放了数据,队列目前有 1 个数据
Thread-0 准备放数据!
Thread-0 已经放了数据,队列目前有 2 个数据
Thread-1 准备放数据!
Thread-1 已经放了数据,队列目前有 3 个数据
Thread-2 准备取数据!
Thread-2 已经取走数据,队列目前有 3 个数据
Thread-0 准备放数据! Thread-1 准备放数据! Thread-2 准备取数据!
Thread-2 已经取走数据,队列目前有 3 个数据
…………………
LinkedBlockingQueue 阻塞队列
LinkedBlockingQueue 类图
LinkedBlockingQueue 中也有两个 Node 分别用来存放首尾节点,并且里面有个初始值为 0 的原子变量 count 用来记录队列元素个数,另外里面有两个 ReentrantLock 的独占锁,分别用来控制元素入队和出队加锁,其中 takeLock 用来控制同时只有一个线程可以从队列获取元素,其他线程必须等待,putLock 控制同时只能有一个线程可以获取锁去添加元素,其他线程必须等待。另外 notEmpty 和 notFull 用来实现入队和出队的同步。 另外由于出入队是两个非公平独占锁,所以可以同时又一个线程入队和一个线程出队,其实这个是个生产者-消费者模型,如下类图:
/** 通过 take 取出进行加锁、取出 */
private final ReentrantLock takeLock = new ReentrantLock();
/** 等待中的队列等待取出 */
private final Condition notEmpty = takeLock.newCondition();
/通过 put 放置进行加锁、放置/
private final ReentrantLock putLock = new ReentrantLock();
/** 等待中的队列等待放置 */
private final Condition notFull = putLock.newCondition();
/* 记录集合中的个数(计数器) */
private final AtomicInteger count = new AtomicInteger(0);
153
队列的容量:
//队列初始容量,Integer 最大值
public static final int MAX_VALUE = 0x7fffffff;
public LinkedBlockingQueue() { this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException(); this.capacity = capacity;
//初始化首尾节点
last = head = new Node(null);
}
如图默认队列容量为 0x7fffffff;用户也可以自己指定容量。
LinkedBlockingQueue 方法
ps:下面介绍 LinkedBlockingQueue 用到很多 Lock 对象。详细可以查找 Lock 对象的介绍
带时间的 Offer 操作-生产者
在 ArrayBlockingQueue 中已经简单介绍了 Offer()方法,LinkedBlocking 的 Offer 方法类似,在此就不过多去介绍。这次我们从介绍下带时间的 Offer 方法
154
//nanos<=0 直接返回
if (nanos <= 0) return false;
//否者调用 await 进行等待,超时则返回<=0(1)
nanos = notFull.awaitNanos(nanos);
}
//await 在超时时间内返回则添加元素(2)
enqueue(new Node(e));
c = count.getAndIncrement();
//队列不满则激活其他等待入队线程(3) if (c + 1 < capacity)
notFull.signal();
} finally {
//释放锁putLock.unlock();
}
//c==0 说明队列里面有一个元素,这时候唤醒出队线程(4)
if (c == 0)
signalNotEmpty(); return true;
}
private void enqueue(Node node) { last = last.next = node;
}
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock; takeLock.lock();
try { notEmpty.signal();
} finally {
takeLock.unlock();
}
}
带时间的 poll 操作-消费者
获取并移除队首元素,在指定的时间内去轮询队列看有没有首元素有则返回,否者超时后返回 null。
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
E x = null;
155
int c = -1;
long nanos = unit.toNanos(timeout); final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
//出队线程获取独占锁takeLock.lockInterruptibly();
try {
//循环直到队列不为空
while (count.get() == 0) {
//超时直接返回 null if (nanos <= 0)
return null;
nanos = notEmpty.awaitNanos(nanos);
}
//出队,计数器减一x = dequeue();
c = count.getAndDecrement();
//如果出队前队列不为空则发送信号,激活其他阻塞的出队线程if (c > 1)
notEmpty.signal();
} finally {
//释放锁takeLock.unlock();
}
//当前队列容量为最大值-1 则激活入队线程。
if (c == capacity) signalNotFull();
return x;
}
首先获取独占锁,然后进入循环当当前队列有元素才会退出循环,或者超时了,直接返回 null。
超时前退出循环后,就从队列移除元素,然后计数器减去一,如果减去 1 前队列元素大于 1 则说明当前移除后队列还有元素,那么就发信号激活其他可能阻塞到当前条件信号的线程。
最后如果减去 1 前队列元素个数=最大值,那么移除一个后会腾出一个空间来,这时候可以激活可能存在的入队
156