JAVA前往高级开发架构方向随笔(第二天)

Java集合详解

 

一、数组和集合的比较

数组不是面向对象的,存在明显的缺陷,集合弥补了数组的缺点,比数组更灵活更实用,而且不同的集合框架类可适用不同场合。如下:
1:数组能存放基本数据类型和对象,而集合类存放的都是对象的引用,而非对象本身!
2:数组容易固定无法动态改变,集合类容量动态改变。 
3:数组无法判断其中实际存有多少元素,length只告诉了数组的容量,而集合的size()可以确切知道元素的个数 
4:集合有多种实现方式和不同适用场合,不像数组仅采用顺序表方式 
5:集合以类的形式存在,具有封装、继承、多态等类的特性,通过简单的方法和属性即可实现各种复杂操作,大大提高了软件的开发效率

Collection和Map,是集合框架的根接口。

Collection的子接口:

Set:接口 ---实现类: HashSet、LinkedHashSet
   Set的子接口SortedSet接口---实现类:TreeSet
List:接口---实现类: LinkedList,Vector,ArrayList 

 

List集合

有序列表,允许存放重复的元素; 
实现类: 
ArrayList:数组实现,查询快,增删慢,轻量级;(线程不安全)
LinkedList:双向链表实现,增删快,查询慢 (线程不安全)
Vector:数组实现,重量级  (线程安全、使用少)

ArrayList

底层是Object数组,所以ArrayList具有数组的查询速度快的优点以及增删速度慢的缺点。

而在LinkedList的底层是一种双向循环链表。在此链表上每一个数据节点都由三部分组成:前指针(指向前面的节点的位置),数据,后指针(指向后面的节点的位置)。最后一个节点的后指针指向第一个节点的前指针,形成一个循环。

双向循环链表的查询效率低但是增删效率高。

ArrayList和LinkedList在用法上没有区别,但是在功能上还是有区别的。

LinkedList

LinkedList是采用双向循环链表实现的。
利用LinkedList实现栈(stack)、队列(queue)、双向队列(double-ended queue )。
它具有方法addFirst()、addLast()、getFirst()、getLast()、removeFirst()、removeLast()等。

经常用在增删操作较多而查询操作很少的情况下:

队列和堆栈。

队列:先进先出的数据结构。

栈:后进先出的数据结构。

注意:使用栈的时候一定不能提供方法让不是最后一个元素的元素获得出栈的机会。

Vector

(与ArrayList相似,区别是Vector是重量级的组件,使用使消耗的资源比较多。)

结论:在考虑并发的情况下用Vector(保证线程的安全)。

在不考虑并发的情况下用ArrayList(不能保证线程的安全)。

ArrayList自动扩充机制
实现机制:ArrayList.ensureCapacity(int minCapacity)
首先得到当前elementData 属性的长度oldCapacity。
然后通过判断oldCapacity和minCapacity参数谁大来决定是否需要扩容, 如果minCapacity大于 
oldCapacity,那么我们就对当前的List对象进行扩容。
扩容的的策略为:取(oldCapacity * 3)/2 + 1和minCapacity之间更大的那个。然后使用数组拷 
贝的方法,把以前存放的数据转移到新的数组对象中
如果minCapacity不大于oldCapacity那么就不进行扩容。


用LinkedList实现队列:
队列(Queue)是限定所有的插入只能在表的一端进行,而所有的删除都在表的另一端进行的线性表。
表中允许插入的一端称为队尾(Rear),允许删除的一端称为队头(Front)。
队列的操作是按先进先出(FIFO)的原则进行的。
队列的物理存储可以用顺序存储结构,也可以用链式存储结构。

用LinkedList实现栈:
栈(Stack)也是一种特殊的线性表,是一种后进先出(LIFO)的结构。
栈是限定仅在表尾进行插入和删除运算的线性表,表尾称为栈顶(top),表头称为栈底(bottom)。
栈的物理存储可以用顺序存储结构,也可以用链式存储结构。

 

List常用方法:
void add(int index, Object element) :添加对象element到位置index上
boolean addAll(int index, Collection collection) :在index位置后添加容器collection中所有的元素
Object get(int index) :取出下标为index的位置的元素
int indexOf(Object element) :查找对象element 在List中第一次出现的位置
int lastIndexOf(Object element) :查找对象element 在List中最后出现的位置
Object remove(int index) :删除index位置上的元素 
ListIterator listIterator(int startIndex) :返回一个ListIterator 跌代器,开始位置为startIndex 
List subList(int fromIndex, int toIndex) :返回一个子列表List ,元素存放为从 fromIndex 到toIndex之前的一个元素

Set集合

扩展Collection接口
无序集合,不允许存放重复的元素;允许使用null元素
对 add()、equals() 和 hashCode() 方法添加了限制
HashSet和TreeSet是Set的实现
Set—》hashSet linkedHashSet
SortedSet —》 TreeSet

HashSet 的后台有一个HashMap;初始化后台容量;只不过生成一个HashSet的话,系统只提供key的访问; 如果有两个Key重复,那么会覆盖之前的;

 
实现类 :
HashSet:equals返回true,hashCode返回相同的整数;哈希表;存储的数据是无序的。
LinkedHashSet:此实现与 HashSet 的不同之外在于,后者维护着一个运行于所有条目的双重链接列表。存储的数据是有序的。

 

HashSet类

HashSet类直接实现了Set接口, 其底层其实是包装了一个HashMap去实现的。HashSet采用HashCode算法来存取集合中的元素,因此具有比较好的读取和查找性能。

 

HashSet的特征

  • 不仅不能保证元素插入的顺序,而且在元素在以后的顺序中也可能变化(这是由HashSet按HashCode存储对象(元素)决定的,对象变化则可能导致HashCode变化)

  • HashSet是线程非安全的

  • HashSet元素值可以为NULL


HashSet常用方法:
public boolean contains(Object o) :如果set包含指定元素,返回true 
public Iterator iterator()返回set中元素的迭代器 
public Object[] toArray() :返回包含set中所有元素的数组public Object[] toArray(Object[] a) :返回包含set中所有元素的数组,返回数组的运行时类型是指定数组的运行时类型
public boolean add(Object o) :如果set中不存在指定元素,则向set加入
public boolean remove(Object o) :如果set中存在指定元素,则从set中删除 
public boolean removeAll(Collection c) :如果set包含指定集合,则从set中删除指定集合的所有元素 
public boolean containsAll(Collection c) :如果set包含指定集合的所有元素,返回true。如果指定集合也是一个set,只有是当前set的子集时,方法返回true

实现Set接口的HashSet,依靠HashMap来实现的。
我们应该为要存放到散列表的各个对象定义hashCode()和equals()。

HashSet的equals和HashCode

前面说过,Set集合是不允许重复元素的,否则将会引发各种奇怪的问题。那么HashSet如何判断元素重复呢?

HashSet需要同时通过equals和HashCode来判断两个元素是否相等,具体规则是,如果两个元素通过equals为true,并且两个元素的hashCode相等,则这两个元素相等(即重复)。

Java 四种线程池的使用

1,线程池的作用 
线程池作用就是限制系统中执行线程的数量。 
根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果。 
少了浪费了系统资源,多了造成系统拥挤效率不高。 
用线程池控制线程数量,其他线程排 队等候。 
一个任务执行完毕,再从队列的中取最前面的任务开始执行。 
若队列中没有等待进程,线程池的这一资源处于等待。 
当一个新任务需要运行时,如果线程池 中有等待的工作线程,就可以开始运行了;否则进入等待队列。 
2,为什么要用线程池? 
1.减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。 
2.可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。 
Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。 
3,比较重要的几个类

类 
描述

ExecutorService 
真正的线程池接口。

ScheduledExecutorService 
能和Timer/TimerTask类似,解决那些需要任务重复执行的问题。

ThreadPoolExecutor 
ExecutorService的默认实现。

ScheduledThreadPoolExecutor 
继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。

4,new Thread的弊端

 
  1. public class TestNewThread {

  2.  
  3. public static void main(String[] args) {

  4. new Thread(new Runnable() {

  5.  
  6. @Override

  7. public void run() {

  8. System.out.println("start");

  9. }

  10. }).start();

  11. }

  12. }

执行一个异步任务你还只是如下new Thread吗? 
那你就out太多了,new Thread的弊端如下: 
1.每次new Thread新建对象性能差。 
2.线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。 
3.缺乏更多功能,如定时执行、定期执行、线程中断。 
相比new Thread,Java提供的四种线程池的好处在于: 
1.重用存在的线程,减少对象创建、消亡的开销,性能佳。 
2.可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。 
3.提供定时执行、定期执行、单线程、并发数控制等功能。

四种线程池 
Java通过Executors提供四种线程池,分别为: 
1,newCachedThreadPoo 
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。 
2,newFixedThreadPool 
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。 
3,newScheduledThreadPool 
创建一个定长线程池,支持定时及周期性任务执行。 
4,newSingleThreadExecutor 
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

源码分析

newCachedThreadPool 
这是一个可缓存线程池,可以灵活的回收空闲线程,无可回收线程时,新建线程 
public static ExecutorService newCachedThreadPool() { 
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 
60L, TimeUnit.SECONDS, 
new SynchronousQueue()); 
}码可以看出底层调用的是ThreadPoolExecutor方法,传入一个同步的阻塞队列实现

 
  1. ThreadPoolExecupublic ThreadPoolExecutor(int corePoolSize,

  2. int maximumPoolSize,

  3. long keepAliveTime,

  4. TimeUnit unit,

  5. BlockingQueue<Runnable> workQueue) {

  6. this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,

  7. Executors.defaultThreadFactory(), defaultHandler);

  8. }

通过源码可以看出,我们可以传入线程池的核心线程数(最小线程数),最大线程数量,保持时间,时间单位,阻塞队列这些参数,最大线程数设置为jvm可用的cpu数量为最佳实践

newWorkStealingPool 
创建持有足够线程的线程池来并行,通过使用多个队列减少竞争,不传参数,则默认设定为cpu的数量 
源码:

 
  1. public static ExecutorService newWorkStealingPool() {

  2. return new ForkJoinPool

  3. (Runtime.getRuntime().availableProcessors(),

  4. ForkJoinPool.defaultForkJoinWorkerThreadFactory,

  5. null, true);

  6. }

通过源码可以看出底层调用的是ForkJoinPool线程池

下面说一下ForkJoinPool

 
  1. public ForkJoinPool(int parallelism,

  2. ForkJoinWorkerThreadFactory factory,

  3. UncaughtExceptionHandler handler,

  4. boolean asyncMode) {

  5. this(checkParallelism(parallelism),

  6. checkFactory(factory),

  7. handler,

  8. asyncMode ? FIFO_QUEUE : LIFO_QUEUE,

  9. "ForkJoinPool-" + nextPoolId() + "-worker-");

  10. checkPermission();

  11. }

使用一个无限队列来保存需要执行的任务,可以传入线程的数量,不传入,则默认使用当前计算机中可用的cpu数量,使用分治法来解决问题,使用fork()和join()来进行调用

newSingleThreadExecutor 
创建一个单线程化的线程池,保证所有任务按照指定的顺序执行(FIFO,LIFO,优先级),当要求进程限制时,可以进行使用

源码:

 
  1. public static ExecutorService newSingleThreadExecutor() {

  2. return new FinalizableDelegatedExecutorService

  3. (new ThreadPoolExecutor(1, 1,

  4. 0L, TimeUnit.MILLISECONDS,

  5. new LinkedBlockingQueue<Runnable>()));

  6. }

newFixedThreadPool 
创建一个固定线程数量,可重用的线程池

源码:

 
  1. public static ExecutorService newFixedThreadPool(int nThreads) {

  2. return new ThreadPoolExecutor(nThreads, nThreads,

  3. 0L, TimeUnit.MILLISECONDS,

  4. new LinkedBlockingQueue<Runnable>());

  5. }

newScheduledThreadPool 
创建一个可定期或者延时执行任务的线程池

源码:

return new ScheduledThreadPoolExecutor(corePoolSize); 

通过源码可以看出底层调用的是一个ScheduledThreadPoolExecutor,然后传入线程数量

下面来介绍一下ScheduledThreadPoolExecutor

 
  1. public ScheduledThreadPoolExecutor(int corePoolSize) {

  2. super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,

  3. new DelayedWorkQueue());

  4. }

通过源码可以看出底层调用了ThreadPoolExecutor,维护了一个延迟队列,可以传入线程数量,传入延时的时间等参数,下面给出一个demo

 
  1. public static void main(String[] args) {

  2. ScheduledExecutorService pool = Executors.newScheduledThreadPool(5);

  3. for (int i = 0; i < 15; i = i + 5) {

  4. pool.schedule(() -> System.out.println("我被执行了,当前时间" + new Date()), i, TimeUnit.SECONDS);

  5. }

  6. pool.shutdown();

  7. }

执行结果

我被执行了,当前时间Fri Jan 12 11:20:41 CST 2018 
我被执行了,当前时间Fri Jan 12 11:20:46 CST 2018 
我被执行了,当前时间Fri Jan 12 11:20:51 CST 2018

有的小伙伴可能会用疑问,为什么使用schedule()而不使用submit()或者execute()呢,下面通过源码来分析

 
  1. public void execute(Runnable command) {

  2. schedule(command, 0, NANOSECONDS);

  3. }

  4. public Future<?> submit(Runnable task) {

  5. return schedule(task, 0, NANOSECONDS);

  6. }

通过源码可以发现这两个方法都是调用的schedule(),而且将延时时间设置为了0,所以想要实现延时操作,需要直接调用schedule()

下面我们再来分析一下submit()和execute()的以及shutdown()和shutdownNow()的区别

submit(),提交一个线程任务,可以接受回调函数的返回值吗,适用于需要处理返回着或者异常的业务场景 
execute(),执行一个任务,没有返回值 
shutdown(),表示不再接受新任务,但不会强行终止已经提交或者正在执行中的任务 
shutdownNow(),对于尚未执行的任务全部取消,正在执行的任务全部发出interrupt(),停止执行 
五种线程池的适应场景 
newCachedThreadPool:用来创建一个可以无限扩大的线程池,适用于服务器负载较轻,执行很多短期异步任务。 
newFixedThreadPool:创建一个固定大小的线程池,因为采用无界的阻塞队列,所以实际线程数量永远不会变化,适用于可以预测线程数量的业务中,或者服务器负载较重,对当前线程数量进行限制。 
newSingleThreadExecutor:创建一个单线程的线程池,适用于需要保证顺序执行各个任务,并且在任意时间点,不会有多个线程是活动的场景。 
newScheduledThreadPool:可以延时启动,定时启动的线程池,适用于需要多个后台线程执行周期任务的场景。 
newWorkStealingPool:创建一个拥有多个任务队列的线程池,可以减少连接数,创建当前可用cpu数量的线程来并行执行,适用于大耗时的操作,可以并行来执行

RejectedExecutionHandler 线程池四种拒绝任务策略

《Java线程池》:任务拒绝策略 
在没有分析线程池原理之前先来分析下为什么有任务拒绝的情况发生。

这里先假设一个前提:线程池有一个任务队列,用于缓存所有待处理的任务,正在处理的任务将从任务队列中移除。因此在任务队列长度有限的情况下就会出现新任务的拒绝处理问题,需要有一种策略来处理应该加入任务队列却因为队列已满无法加入的情况。另外在线程池关闭的时候也需要对任务加入队列操作进行额外的协调处理。

RejectedExecutionHandler提供了四种方式来处理任务拒绝策略

1、直接丢弃(DiscardPolicy)

2、丢弃队列中最老的任务(DiscardOldestPolicy)。

3、抛异常(AbortPolicy)

4、将任务分给调用线程来执行(CallerRunsPolicy)。

这四种策略是独立无关的,是对任务拒绝处理的四中表现形式。最简单的方式就是直接丢弃任务。但是却有两种方式,到底是该丢弃哪一个任务,比如可以丢弃当前将要加入队列的任务本身(DiscardPolicy)或者丢弃任务队列中最旧任务(DiscardOldestPolicy)。丢弃最旧任务也不是简单的丢弃最旧的任务,而是有一些额外的处理。除了丢弃任务还可以直接抛出一个异常(RejectedExecutionException),这是比较简单的方式。抛出异常的方式(AbortPolicy)尽管实现方式比较简单,但是由于抛出一个RuntimeException,因此会中断调用者的处理过程。除了抛出异常以外还可以不进入线程池执行,在这种方式(CallerRunsPolicy)中任务将有调用者线程去执行。

示例 
1,newCachedThreadPool 
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程, 那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

 
  1. package io.ymq.thread.demo1;

  2.  
  3. import java.util.concurrent.ExecutorService;

  4. import java.util.concurrent.Executors;

  5.  
  6. /**

  7. * 描述: 创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。

  8. * 此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

  9. *

  10. * @author yanpenglei

  11. * @create 2017-10-12 11:13

  12. **/

  13. public class TestNewCachedThreadPool {

  14. public static void main(String[] args) {

  15. ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

  16. for (int i = 1; i <= 10; i++) {

  17. final int index = i;

  18. try {

  19. Thread.sleep(index * 1000);

  20. } catch (InterruptedException e) {

  21. e.printStackTrace();

  22. }

  23.  
  24. cachedThreadPool.execute(new Runnable() {

  25.  
  26. @Override

  27. public void run() {

  28. String threadName = Thread.currentThread().getName();

  29. System.out.println("执行:" + index + ",线程名称:" + threadName);

  30. }

  31. });

  32. }

  33. }

  34. }

响应: 
执行:1,线程名称:pool-1-thread-1 
执行:2,线程名称:pool-1-thread-1 
执行:3,线程名称:pool-1-thread-1 
执行:4,线程名称:pool-1-thread-1 
执行:5,线程名称:pool-1-thread-1 
执行:6,线程名称:pool-1-thread-1 
执行:7,线程名称:pool-1-thread-1 
执行:8,线程名称:pool-1-thread-1 
执行:9,线程名称:pool-1-thread-1 
执行:10,线程名称:pool-1-thread-1 
2,newFixedThreadPool 
描述:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。 
线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

 
  1. package io.ymq.thread.demo2;

  2.  
  3. import java.util.concurrent.ExecutorService;

  4. import java.util.concurrent.Executors;

  5.  
  6. /**

  7. * 描述:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。

  8. * 线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

  9. *

  10. * @author yanpenglei

  11. * @create 2017-10-12 11:30

  12. **/

  13. public class TestNewFixedThreadPool {

  14.  
  15. public static void main(String[] args) {

  16.  
  17. ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);

  18.  
  19. for (int i = 1; i <= 10; i++) {

  20. final int index = i;

  21. fixedThreadPool.execute(new Runnable() {

  22.  
  23. @Override

  24. public void run() {

  25. try {

  26. String threadName = Thread.currentThread().getName();

  27. System.out.println("执行:" + index + ",线程名称:" + threadName);

  28. Thread.sleep(2000);

  29. } catch (InterruptedException e) {

  30.  
  31. e.printStackTrace();

  32. }

  33. }

  34. });

  35. }

  36.  
  37. }

  38. }

因为线程池大小为3,每个任务输出index后sleep 2秒,所以每两秒打印3个数字,和线程名称。 
响应: 
执行:2,线程名称:pool-1-thread-2 
执行:3,线程名称:pool-1-thread-3 
执行:1,线程名称:pool-1-thread-1

执行:4,线程名称:pool-1-thread-1 
执行:6,线程名称:pool-1-thread-2 
执行:5,线程名称:pool-1-thread-3

执行:7,线程名称:pool-1-thread-1 
执行:9,线程名称:pool-1-thread-3 
执行:8,线程名称:pool-1-thread-2

执行:10,线程名称:pool-1-thread-1

3,newScheduledThreadPool 
创建一个定长线程池,支持定时及周期性任务执行。延迟执行

 
  1. package io.ymq.thread.demo3;

  2.  
  3. import java.util.concurrent.Executors;

  4. import java.util.concurrent.ScheduledExecutorService;

  5. import java.util.concurrent.TimeUnit;

  6.  
  7. /**

  8. * 描述:创建一个定长线程池,支持定时及周期性任务执行。延迟执行

  9. *

  10. * @author yanpenglei

  11. * @create 2017-10-12 11:53

  12. **/

  13. public class TestNewScheduledThreadPool {

  14.  
  15. public static void main(String[] args) {

  16.  
  17. ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);

  18.  
  19. scheduledThreadPool.schedule(new Runnable() {

  20.  
  21. @Override

  22. public void run() {

  23. System.out.println("表示延迟3秒执行。");

  24. }

  25. }, 3, TimeUnit.SECONDS);

  26.  
  27.  
  28. scheduledThreadPool.scheduleAtFixedRate(new Runnable() {

  29.  
  30. @Override

  31. public void run() {

  32. System.out.println("表示延迟1秒后每3秒执行一次。");

  33. }

  34. }, 1, 3, TimeUnit.SECONDS);

  35. }

  36.  
  37. }

表示延迟1秒后每3秒执行一次。 
表示延迟3秒执行。 
表示延迟1秒后每3秒执行一次。 
表示延迟1秒后每3秒执行一次。 
表示延迟1秒后每3秒执行一次。 
表示延迟1秒后每3秒执行一次。 
4,newSingleThreadExecutor 
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

 
  1. package io.ymq.thread.demo4;

  2.  
  3. import java.util.concurrent.ExecutorService;

  4. import java.util.concurrent.Executors;

  5.  
  6. /**

  7. * 描述:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

  8. *

  9. * @author yanpenglei

  10. * @create 2017-10-12 12:05

  11. **/

  12. public class TestNewSingleThreadExecutor {

  13.  
  14. public static void main(String[] args) {

  15. ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();

  16. for (int i = 1; i <= 10; i++) {

  17. final int index = i;

  18. singleThreadExecutor.execute(new Runnable() {

  19.  
  20. @Override

  21. public void run() {

  22. try {

  23. String threadName = Thread.currentThread().getName();

  24. System.out.println("执行:" + index + ",线程名称:" + threadName);

  25. Thread.sleep(2000);

  26. } catch (InterruptedException e) {

  27. e.printStackTrace();

  28. }

  29. }

  30. });

  31. }

  32. }

  33. }

结果依次输出,相当于顺序执行各个任务。 
响应: 
执行:1,线程名称:pool-1-thread-1 
执行:2,线程名称:pool-1-thread-1 
执行:3,线程名称:pool-1-thread-1 
执行:4,线程名称:pool-1-thread-1 
执行:5,线程名称:pool-1-thread-1 
执行:6,线程名称:pool-1-thread-1 
执行:7,线程名称:pool-1-thread-1 
执行:8,线程名称:pool-1-thread-1 
执行:9,线程名称:pool-1-thread-1 
执行:10,线程名称:pool-1-thread-1

Java 对象和类

  • 对象:对象是类的一个实例(对象不是找个女朋友),有状态和行为。例如,一条狗是一个对象,它的状态有:颜色、名字、品种;行为有:摇尾巴、叫、吃等。
  • :类是一个模板,它描述一类对象的行为和状态。
  • Java中的对象

     

    现在让我们深入了解什么是对象。看看周围真实的世界,会发现身边有很多对象,车,狗,人等等。所有这些对象都有自己的状态和行为。

    拿一条狗来举例,它的状态有:名字、品种、颜色,行为有:叫、摇尾巴和跑。

    对比现实对象和软件对象,它们之间十分相似。

    软件对象也有状态和行为。软件对象的状态就是属性,行为通过方法体现。

    在软件开发中,方法操作对象内部状态的改变,对象的相互调用也是通过方法来完成。

  • 一个类可以包含以下类型变量:

  • 局部变量:在方法、构造方法或者语句块中定义的变量被称为局部变量。变量声明和初始化都是在方法中,方法结束后,变量就会自动销毁。
  • 成员变量:成员变量是定义在类中,方法体之外的变量。这种变量在创建对象的时候实例化。成员变量可以被类中方法、构造方法和特定类的语句块访问。
  • 类变量:类变量也声明在类中,方法体之外,但必须声明为static类型。
  • 每个类都有构造方法。如果没有显式地为类定义构造方法,Java编译器将会为该类提供一个默认构造方法。

    在创建一个对象的时候,至少要调用一个构造方法。构造方法的名称必须与类同名,一个类可以有多个构造方法。

  •  

     

    一个类可以拥有多个方法,在上面的例子中:barking()、hungry()和sleeping()都是Dog类的方法。

    构造方法

创建对象

对象是根据类创建的。在Java中,使用关键字new来创建一个新的对象。创建对象需要以下三步:

  • 声明:声明一个对象,包括对象名称和对象类型。
  • 实例化:使用关键字new来创建一个对象。
  • 初始化:使用new创建对象时,会调用构造方法初始化对象。
  • 源文件声明规则

    在本节的最后部分,我们将学习源文件的声明规则。当在一个源文件中定义多个类,并且还有import语句和package语句时,要特别注意这些规则。

  • 一个源文件中只能有一个public类
  • 一个源文件可以有多个非public类
  • 源文件的名称应该和public类的类名保持一致。例如:源文件中public类的类名是Employee,那么源文件应该命名为Employee.java。
  • 如果一个类定义在某个包中,那么package语句应该在源文件的首行。
  • 如果源文件包含import语句,那么应该放在package语句和类定义之间。如果没有package语句,那么import语句应该在源文件中最前面。
  • import语句和package语句对源文件中定义的所有类都有效。在同一源文件中,不能给不同的类不同的包声明。
  • 类有若干种访问级别,并且类也分不同的类型:抽象类和final类等。这些将在访问控制章节介绍。

    除了上面提到的几种类型,Java还有一些特殊的类,如:内部类、匿名类。


    Java包

    包主要用来对类和接口进行分类。当开发Java程序时,可能编写成百上千的类,因此很有必要对类和接口进行分类。

  • Java 基本数据类型

  • Java 变量类型

  • Java语言支持的变量类型有:

  • 类变量:独立于方法之外的变量,用 static 修饰。
  • 实例变量:独立于方法之外的变量,不过没有 static 修饰。
  • 局部变量:类的方法中的变量。
  • Java 局部变量

  • 局部变量声明在方法、构造方法或者语句块中;
  • 局部变量在方法、构造方法、或者语句块被执行的时候创建,当它们执行完成后,变量将会被销毁;
  • 访问修饰符不能用于局部变量;
  • 局部变量只在声明它的方法、构造方法或者语句块中可见;
  • 局部变量是在栈上分配的。
  • 局部变量没有默认值,所以局部变量被声明后,必须经过初始化,才可以使用。

实例变量

  • 实例变量声明在一个类中,但在方法、构造方法和语句块之外;
  • 当一个对象被实例化之后,每个实例变量的值就跟着确定;
  • 实例变量在对象创建的时候创建,在对象被销毁的时候销毁;
  • 实例变量的值应该至少被一个方法、构造方法或者语句块引用,使得外部能够通过这些方式获取实例变量信息;
  • 实例变量可以声明在使用前或者使用后;
  • 访问修饰符可以修饰实例变量;
  • 实例变量对于类中的方法、构造方法或者语句块是可见的。一般情况下应该把实例变量设为私有。通过使用访问修饰符可以使实例变量对子类可见;
  • 实例变量具有默认值。数值型变量的默认值是0,布尔型变量的默认值是false,引用类型变量的默认值是null。变量的值可以在声明时指定,也可以在构造方法中指定;
  • 实例变量可以直接通过变量名访问。但在静态方法以及其他类中,就应该使用完全限定名:ObejectReference.VariableName。
  • 类变量(静态变量)

  • 类变量也称为静态变量,在类中以 static 关键字声明,但必须在方法之外。
  • 无论一个类创建了多少个对象,类只拥有类变量的一份拷贝。
  • 静态变量除了被声明为常量外很少使用。常量是指声明为public/private,final和static类型的变量。常量初始化后不可改变。
  • 静态变量储存在静态存储区。经常被声明为常量,很少单独使用static声明变量。
  • 静态变量在第一次被访问时创建,在程序结束时销毁。
  • 与实例变量具有相似的可见性。但为了对类的使用者可见,大多数静态变量声明为public类型。
  • 默认值和实例变量相似。数值型变量默认值是0,布尔型默认值是false,引用类型默认值是null。变量的值可以在声明的时候指定,也可以在构造方法中指定。此外,静态变量还可以在静态语句块中初始化。
  • 静态变量可以通过:ClassName.VariableName的方式访问。
  • 类变量被声明为public static final类型时,类变量名称一般建议使用大写字母。如果静态变量不是public和final类型,其命名方式与实例变量以及局部变量的命名方式一致。

Java 修饰符

Java语言提供了很多修饰符,主要分为以下两类:

  • 访问修饰符
  • 非访问修饰符

修饰符用来定义类、方法或者变量,通常放在语句的最前端。我们通过下面的例子来说明:

public class className { // ... } private boolean myFlag; static final double weeks = 9.5; protected static final int BOXWIDTH = 42; public static void main(String[] arguments) { // 方法体 }


访问控制修饰符

Java中,可以使用访问控制符来保护对类、变量、方法和构造方法的访问。Java 支持 4 种不同的访问权限。

  • default (即缺省,什么也不写): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。

  • private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)

  • public : 对所有类可见。使用对象:类、接口、变量、方法

  • protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)

我们可以通过以下表来说明访问权限:

访问控制
修饰符当前类同一包内子孙类(同一包)子孙类(不同包)其他包
publicYYYYY
protectedYYYY/N(说明N
defaultYYYNN
privateYNNNN

默认访问修饰符-不使用任何关键字

使用默认访问修饰符声明的变量和方法,对同一个包内的类是可见的。接口里的变量都隐式声明为 public static final,而接口里的方法默认情况下访问权限为 public。

如下例所示,变量和方法的声明可以不使用任何修饰符。

实例

String version = "1.5.1"; boolean processOrder() { return true; }

私有访问修饰符-private

私有访问修饰符是最严格的访问级别,所以被声明为 private 的方法、变量和构造方法只能被所属类访问,并且类和接口不能声明为 private

声明为私有访问类型的变量只能通过类中公共的 getter 方法被外部类访问。

Private 访问修饰符的使用主要用来隐藏类的实现细节和保护类的数据。

下面的类使用了私有访问修饰符:

public class Logger { private String format; public String getFormat() { return this.format; } public void setFormat(String format) { this.format = format; } }

实例中,Logger 类中的 format 变量为私有变量,所以其他类不能直接得到和设置该变量的值。为了使其他类能够操作该变量,定义了两个 public 方法:getFormat() (返回 format的值)和 setFormat(String)(设置 format 的值)

公有访问修饰符-public

被声明为 public 的类、方法、构造方法和接口能够被任何其他类访问。

如果几个相互访问的 public 类分布在不同的包中,则需要导入相应 public 类所在的包。由于类的继承性,类所有的公有方法和变量都能被其子类继承。

以下函数使用了公有访问控制:

public static void main(String[] arguments) { // ... }

Java 程序的 main() 方法必须设置成公有的,否则,Java 解释器将不能运行该类。

受保护的访问修饰符-protected

protected 需要从以下两个点来分析说明:

  • 子类与基类在同一包中:被声明为 protected 的变量、方法和构造器能被同一个包中的任何其他类访问;

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

protected 可以修饰数据成员,构造方法,方法成员,不能修饰类(内部类除外)

接口及接口的成员变量和成员方法不能声明为 protected。 可以看看下图演示:

子类能访问 protected 修饰符声明的方法和变量,这样就能保护不相关的类使用这些方法和变量。

下面的父类使用了 protected 访问修饰符,子类重写了父类的 openSpeaker() 方法。

class AudioPlayer { protected boolean openSpeaker(Speaker sp) { // 实现细节 } } class StreamingAudioPlayer extends AudioPlayer { protected boolean openSpeaker(Speaker sp) { // 实现细节 } }

如果把 openSpeaker() 方法声明为 private,那么除了 AudioPlayer 之外的类将不能访问该方法。

如果把 openSpeaker() 声明为 public,那么所有的类都能够访问该方法。

如果我们只想让该方法对其所在类的子类可见,则将该方法声明为 protected。

 

protected 是最难理解的一种 Java 类成员访问权限修饰词,更多详细内容请查看 Java protected 关键字详解

访问控制和继承

请注意以下方法继承的规则:

  • 父类中声明为 public 的方法在子类中也必须为 public。

  • 父类中声明为 protected 的方法在子类中要么声明为 protected,要么声明为 public,不能声明为 private。

  • 父类中声明为 private 的方法,不能够被继承。


非访问修饰符

为了实现一些其他的功能,Java 也提供了许多非访问修饰符。

static 修饰符,用来修饰类方法和类变量。

final 修饰符,用来修饰类、方法和变量,final 修饰的类不能够被继承,修饰的方法不能被继承类重新定义,修饰的变量为常量,是不可修改的。

abstract 修饰符,用来创建抽象类和抽象方法。

synchronized 和 volatile 修饰符,主要用于线程的编程。

static 修饰符

  • 静态变量:

    static 关键字用来声明独立于对象的静态变量,无论一个类实例化多少对象,它的静态变量只有一份拷贝。 静态变量也被称为类变量。局部变量不能被声明为 static 变量。

  • 静态方法:

    static 关键字用来声明独立于对象的静态方法。静态方法不能使用类的非静态变量。静态方法从参数列表得到数据,然后计算这些数据。

对类变量和方法的访问可以直接使用 classname.variablename 和 classname.methodname 的方式访问。

如下例所示,static修饰符用来创建类方法和类变量。

public class InstanceCounter { private static int numInstances = 0; protected static int getCount() { return numInstances; } private static void addInstance() { numInstances++; } InstanceCounter() { InstanceCounter.addInstance(); } public static void main(String[] arguments) { System.out.println("Starting with " + InstanceCounter.getCount() + " instances"); for (int i = 0; i < 500; ++i){ new InstanceCounter(); } System.out.println("Created " + InstanceCounter.getCount() + " instances"); } }

以上实例运行编辑结果如下:

Starting with 0 instances
Created 500 instances

final 修饰符

final 变量:

final 表示"最后的、最终的"含义,变量一旦赋值后,不能被重新赋值。被 final 修饰的实例变量必须显式指定初始值。

final 修饰符通常和 static 修饰符一起使用来创建类常量。

实例

public class Test{ final int value = 10; // 下面是声明常量的实例 public static final int BOXWIDTH = 6; static final String TITLE = "Manager"; public void changeValue(){ value = 12; //将输出一个错误 } }

final 方法

类中的 final 方法可以被子类继承,但是不能被子类修改。

声明 final 方法的主要目的是防止该方法的内容被修改。

如下所示,使用 final 修饰符声明方法。

public class Test{ public final void changeName(){ // 方法体 } }

final 类

final 类不能被继承,没有类能够继承 final 类的任何特性。

实例

public final class Test { // 类体 }

abstract 修饰符

抽象类:

抽象类不能用来实例化对象,声明抽象类的唯一目的是为了将来对该类进行扩充。

一个类不能同时被 abstract 和 final 修饰。如果一个类包含抽象方法,那么该类一定要声明为抽象类,否则将出现编译错误。

抽象类可以包含抽象方法和非抽象方法。

实例

abstract class Caravan{ private double price; private String model; private String year; public abstract void goFast(); //抽象方法 public abstract void changeColor(); }

抽象方法

抽象方法是一种没有任何实现的方法,该方法的的具体实现由子类提供。

抽象方法不能被声明成 final 和 static。

任何继承抽象类的子类必须实现父类的所有抽象方法,除非该子类也是抽象类。

如果一个类包含若干个抽象方法,那么该类必须声明为抽象类。抽象类可以不包含抽象方法。

抽象方法的声明以分号结尾,例如:public abstract sample();

实例

public abstract class SuperClass{ abstract void m(); //抽象方法 } class SubClass extends SuperClass{ //实现抽象方法 void m(){ ......... } }

synchronized 修饰符

synchronized 关键字声明的方法同一时间只能被一个线程访问。synchronized 修饰符可以应用于四个访问修饰符。

实例

public synchronized void showDetails(){ ....... }

transient 修饰符

序列化的对象包含被 transient 修饰的实例变量时,java 虚拟机(JVM)跳过该特定的变量。

该修饰符包含在定义变量的语句中,用来预处理类和变量的数据类型。

实例

public transient int limit = 55; // 不会持久化 public int b; // 持久化

volatile 修饰符

volatile 修饰的成员变量在每次被线程访问时,都强制从共享内存中重新读取该成员变量的值。而且,当成员变量发生变化时,会强制线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。

一个 volatile 对象引用可能是 null。

实例

public class MyRunnable implements Runnable { private volatile boolean active; public void run() { active = true; while (active) // 第一行 { // 代码 } } public void stop() { active = false; // 第二行 } }

通常情况下,在一个线程调用 run() 方法(在 Runnable 开启的线程),在另一个线程调用 stop() 方法。 如果 第一行 中缓冲区的 active 值被使用,那么在 第二行 的 active 值为 false 时循环不会停止。

但是以上代码中我们使用了 volatile 修饰 active,所以该循环会停止。

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值