这篇博客是自己平时看到的一些或者有新知识点,或者旧知识重新学习之后有启发的一些内容,总结在这里,也是为了方便日后回顾,共勉。因为大部分知识点阅读起来需要些基础,所以初步读起来可能会有些困难,可根据此处的知识点再去搜索引擎做更详细的搜索。
静态变量
、常量以及编译器编译后的代码等大多数 JVM 将内存区域划分为 Method Area(Non-Heap)(方法区) ,Heap(堆) , Program Counter Register(程序计数器) , VM Stack(虚拟机栈,也有翻译成JAVA 方法栈的),Native Method Stack ( 本地方法栈 ),其中Method Area 和 Heap 是线程共享的 ,VM Stack,Native Method Stack 和Program Counter Register 是非线程共享的。为什么分为 线程共享和非线程共享的呢?请继续往下看。
首先我们熟悉一下一个一般性的 Java 程序的工作过程。一个 Java 源程序文件,会被编译为字节码文件(以 class 为扩展名),每个java程序都需要运行在自己的JVM上,然后告知 JVM 程序的运行入口,再被 JVM 通过字节码解释器加载运行。那么程序开始运行后,都是如何涉及到各内存区域的呢?
概括地说来,JVM初始运行的时候都会分配好 Method Area(方法区) 和Heap(堆) ,而JVM 每遇到一个线程,就为其分配一个 Program Counter Register(程序计数器) , VM Stack(虚拟机栈)和Native Method Stack (本地方法栈), 当线程终止时,三者(虚拟机栈,本地方法栈和程序计数器)所占用的内存空间也会被释放掉。这也是为什么我把内存区域分为线程共享和非线程共享的原因,非线程共享的那三个区域的生命周期与所属线程相同,而线程共享的区域与JAVA程序运行的生命周期相同,所以这也是系统垃圾回收的场所只发生在线程共享的区域(实际上对大部分虚拟机来说知发生在Heap上)的原因。
PreparedStatement是预编译的,使用PreparedStatement有几个好处
抽象类
给出了设计 servlet 的一些骨架,定义了 servlet 生命周期,还有一些得到名字、配置、初始化参数的方法,其设计的是和应用层协议无关的servlet在多线程下其本身并不是线程安全的。
如果在类中定义成员变量,而在service中根据不同的线程对该成员变量进行更改,那么在并发的时候就会引起错误。最好是在方法中,定义局部变量,而不是类变量或者对象的成员变量。由于方法中的局部变量是在栈中,彼此各自都拥有独立的运行空间而不会互相干扰,因此才做到线程安全。
Action 类:
• Struts1要求Action类继承一个抽象基类。Struts1的一个普遍问题是使用抽象类编程而不是接口,而struts2的Action是接口。
• Struts 2 Action类可以实现一个Action接口,也可实现其他接口,使可选和定制的服务成为可能。Struts2提供一个ActionSupport基类去 实现 常用的接口。Action接口不是必须的,任何有execute标识的POJO对象都可以用作Struts2的Action对象。
线程模式:
• Struts1 Action是单例模式并且必须是线程安全的,因为仅有Action的一个实例来处理所有的请求。单例策略限制了Struts1 Action能作的事,并且要在开发时特别小心。Action资源必须是线程安全的或同步的。
• Struts2 Action对象为每一个请求产生一个实例,因此没有线程安全问题。(实际上,servlet容器给每个请求产生许多可丢弃的对象,并且不会导致性能和垃圾回收问题)
Servlet 依赖:
• Struts1 Action 依赖于Servlet API ,因为当一个Action被调用时HttpServletRequest 和 HttpServletResponse 被传递给execute方法。
• Struts 2 Action不依赖于容器,允许Action脱离容器单独被测试。如果需要,Struts2 Action仍然可以访问初始的request和response。但是,其他的元素减少或者消除了直接访问HttpServetRequest 和 HttpServletResponse的必要性。
可测性:
• 测试Struts1 Action的一个主要问题是execute方法暴露了servlet API(这使得测试要依赖于容器)。一个第三方扩展--Struts TestCase--提供了一套Struts1的模拟对象(来进行测试)。
• Struts 2 Action可以通过初始化、设置属性、调用方法来测试,“依赖注入”支持也使测试更容易。
捕获输入:
• Struts1 使用ActionForm对象捕获输入。所有的ActionForm必须继承一个基类。因为其他JavaBean不能用作ActionForm,开发者经常创建多余的类捕获输入。动态Bean(DynaBeans)可以作为创建传统ActionForm的选择,但是,开发者可能是在重新描述(创建)已经存 在的JavaBean(仍然会导致有冗余的javabean)。
• Struts 2直接使用Action属性作为输入属性,消除了对第二个输入对象的需求。输入属性可能是有自己(子)属性的rich对象类型。Action属性能够通过 web页面上的taglibs访问。Struts2也支持ActionForm模式。rich对象类型,包括业务对象,能够用作输入/输出对象。这种 ModelDriven 特性简化了taglib对POJO输入对象的引用。
表达式语言:
• Struts1 整合了JSTL,因此使用JSTL EL。这种EL有基本对象图遍历,但是对集合和索引属性的支持很弱。
• Struts2可以使用JSTL,但是也支持一个更强大和灵活的表达式语言--“Object Graph Notation Language” (OGNL).
封装是把对象的属性和操作结合在一起,组成一个新的独立的对象,其内部信息对外界是隐藏的,外界只能通过有限的接口域对象发生关系
类的初始化顺序遵循两个规则:(方便记忆)
一、父类先于子类
二、静态先于非静态
我们根据这两个规则:有初始化顺序–>
**解释:**其中依赖倒置原则是一种设计思想,该思想的核心理念是高层的代码不应该依赖于底层的代码。并基于此思想设计出了IOC控制反转,IOC的实现需要通过依赖注入的方法实现,即DI是实现IOC的一种方法。
**优势:**避免在各处使用new来创建对象,从而实现对象的统一维护
创建实例的时候不必了解其中的实现细节,通过接口进行实现。
Spring IOC容器的核心接口:
当用户使用Spring的WebApplicationContext时,还可以使用另外3种Bean的作用域,即request,session和globalSession。
面向切面编程
关注点分离,不同的问题交给不同的部分去解决。
分离业务功能代码和切面代码,架构将变变得高内聚低耦合。为确保功能的完整性,切面最终要通过Weave被合并到业务中。
AOP的实现
https://www.cnblogs.com/nullzx/p/8978177.html
-----------------------------------------------------------------2019.6.2---------------------------------------------------------------
switch语句不加break会产生击穿现象,顺序执行case后面的内容,带来性能的损耗。
在switch语句对case判断成功后,不加break,其后的内容将会顺序执行,带来性能问题。
对于同时使用了getter和setter方法的域属性,除在
如上图是Object类的equals方法, 比较的是对象的引用地址。
String类重写的equals方法,比较字符串的值
默认情况下也就是从超类Object继承而来的equals方法与‘==’是完全等价的,比较的都是对象的内存地址,但我们可以重写equals方法,使其按照我们的需求的方式进行比较,如String类重写了equals方法,使其比较的是字符的序列,而不再是内存地址。
根据类的路径获取类的具体信息,并可通过获取的类型信息进行对象实例化
String a = “aaa”+“bbb”;
String b = “aaabbb”;
----->比较结果为true
String a= “aaa”;
String b = “bbb”;
String c = “aaabbb”;
System.out.println((a+b) == c);
----->比较结果为false
subString方法采用的是new的方式产生子串
如对数组进行拷贝的时候使用for循环进行拷贝,和使用Arrays.copyof()方法进行数组拷贝哪个效率更高呢?
我们可以看到copyof方法使用了反射的机制进行数组拷贝,而使用反射会带来极大的性能问题,因为反射需要获取类的信息。所以对于引用数据类型,使用for循环进行拷贝是一种更好的选择。
子类继承父类,使用多态进行实现,Person person = new Student()
若想调用子类的方法,需要将person强转为Student类型
抽象类是对一种事物的抽象,即对类抽象,而接口是对行为的抽象。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。
举个简单的例子,飞机和鸟是不同类的事物,但是它们都有一个共性,就是都会飞。那么在设计的时候,可以将飞机设计为一个类Airplane,将鸟设计为一个类Bird,但是不能将 飞行 这个特性也设计为类,因此它只是一个行为特性,并不是对一类事物的抽象描述。此时可以将 飞行 设计为一个接口Fly,包含方法fly( ),然后Airplane和Bird分别根据自己的需要实现Fly这个接口。然后至于有不同种类的飞机,比如战斗机、民用飞机等直接继承Airplane即可,对于鸟也是类似的,不同种类的鸟直接继承Bird类即可。从这里可以看出,继承是一个 "是不是"的关系,而 接口 实现则是 "有没有"的关系。如果一个类继承了某个抽象类,则子类必定是抽象类的种类,而接口实现则是有没有、具备不具备的关系,比如鸟是否能飞(或者是否具备飞行这个特点),能飞行则可以实现这个接口,不能飞行就不实现这个接口。
Java包装类及自动装箱、拆箱
java会自动调用装箱和拆箱操作,Integer.valueOf(3)和 i.intValue()
public static Integer valueOf(int i) {
if(i >= -128 && i <= IntegerCache.high)
return IntegerCache.cache[i + 128];
else
return new Integer(i);
}
MyISAM不支持外键,不是事务安全的,索引时使用非聚簇索引,数据锁是表级锁。
相反,Innodb支持外键约束,具有事务的提交、回滚和崩溃恢复能力,在索引时使用聚簇索引,数据锁为行锁(并非所有操作都是行锁,在一些条件下仍会锁定全表)。
equals方法原本比较的是对象的地址,此时hashCode也是使用地址进行的比较。
若对equals方法进行了重写,如对其中的某个属性进行比较,但并未重写hashCode方法,就会产生equals方法返回true但hashCode中比较为false的现象。这会在集合框架中,hashMap产生严重的问题
保证equals判断结果为true,那么他们的hashCode必然相同。
对象的实例(instantOopDesc)存储在堆中,对象的元数据(instantKlass)存储在方法区中,对象的引用存储在虚拟机栈中。
方法区用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 所谓加载的类信息,其实不就是给每一个被加载的类都创建了一个 instantKlass对象么。
在HotSpot虚拟机中,使用oop-klass模型来表示对象。每一个Java类,在被JVM加载的时候,JVM会给这个类创建一个instanceKlass,保存在方法区,用来在JVM层表示该Java类。当我们在Java代码中,使用new创建一个对象的时候,JVM会创建一个instanceOopDesc对象,这个对象中包含了对象头以及实例数据。
https://www.hollischuang.com/archives/1910
java对象头使用3位来标记锁,其中1 bit 用于标记偏向锁,剩余 2 bit 共标记四种锁,其中11标记GC。
https://www.hollischuang.com/archives/1953
避免使用Executors创建线程池,主要是避免使用其中的默认实现,那么我们可以自己直接调用ThreadPoolExecutor的构造函数来自己创建线程池。在创建的同时,给BlockQueue指定容量就可以了。
private static ExecutorService executor = new ThreadPoolExecutor(10, 10,
60L, TimeUnit.SECONDS,
new ArrayBlockingQueue(10));
这种情况下,一旦提交的线程数超过当前可用线程数时,就会抛出java.util.concurrent.RejectedExecutionException,这是因为当前线程池使用的队列是有边界队列,队列已经满了便无法继续处理新的请求。但是异常(Exception)总比发生错误(Error)要好。
除了自己定义ThreadPoolExecutor外。还有其他方法。这个时候第一时间就应该想到开源类库,如apache和guava等。
作者推荐使用guava提供的ThreadFactoryBuilder来创建线程池。
public class ExecutorsDemo {
private static ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
.setNameFormat("demo-pool-%d").build();
private static ExecutorService pool = new ThreadPoolExecutor(5, 200,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());
public static void main(String[] args) {
for (int i = 0; i < Integer.MAX_VALUE; i++) {
pool.execute(new SubThread());
}
}
}z`
http://www.hollischuang.com/archives/1246
雪花算法
1bit留空 41bit时间戳 10bit机器码 12bit自增序号
RuntimeTimeException Error 也可使用@Transactional(rollBackFor=“xxException”)注解指定抛出回滚的异常类型
若使用SimpleDateformate用作成员变量且声明为static,则会产生线程安全的问题。如下:
public class Main {
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
simpleDateFormat.setTimeZone(TimeZone.getTimeZone("America/New_York"));
System.out.println(simpleDateFormat.format(Calendar.getInstance().getTime()));
}
}
因为simpleDateFormate中存在着一个私有的calendar属性,在并发的情况下就是一个共享变量,会产生并发问题。
解决方式:
在foreach中使用remove和add方法会导致ConcurrentModificationException的抛出。
原因:
remove仅改变了modCount但未改变expectedModCount的值,因此会导致比较异常的抛出。
短连接: 每次Http请求建立一次连接
长连接: 一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的 TCP连接不会关闭,如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的连接。Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间。实现长连接要客户端和服务端都支持长连接。
Fail-fast机制是java集合框架中的一种异常检测机制,当多线程对集合进行结构性改变时,有可能会产生fail-fast机制。
异常原理:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
如上,在该方法中对modCount和expectedModCount进行了比较,如果二者不想等,则抛出CMException。
那么modCount和expectedModCount又是什么呢?
看下面remove的源码:
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
仅对modCount的值进行了修改,但并未对内部类中的expectedModCount进行修改。因而在对值进行的比较的时候触发了fail-fast机制,导致抛出ConcurrentModificationException异常。
总结: 简单总结一下,之所以会抛出CMException异常,是因为我们的代码中使用了增强for循环,而在增强for循环中,集合遍历是通过iterator进行的,但是元素的add/remove却是直接使用的集合类自己的方法。这就导致iterator在遍历的时候,会发现有一个元素在自己不知不觉的情况下就被删除/添加了,就会抛出一个异常,用来提示用户,可能发生了并发修改!
所以,在使用Java的集合类的时候,如果发生CMException,优先考虑fail-fast有关的情况,实际上这里并没有真的发生并发,只是Iterator使用了fail-fast的保护机制,只要他发现有某一次修改是未经过自己进行的,那么就会抛出异常
https://www.hollischuang.com/archives/3542
底层原理:
首先,我们看下subList方法给我们返回的List到底是个什么东西,这一点在JDK源码中注释是这样说的:
Returns a view of the portion of this list between the specifiedfromIndex, inclusive, and toIndex, exclusive
也就是说subList 返回是一个视图,那么什么叫做视图呢?
我们看下subList的源码:
public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList(this, 0, fromIndex, toIndex);
}
这个方法返回了一个SubList,这个类是ArrayList中的一个内部类。
SubList这个类中单独定义了set、get、size、add、remove等方法。
当我们调用subList方法的时候,会通过调用SubList的构造函数创建一个SubList,那么看下这个构造函数做了哪些事情:
SubList(AbstractList<E> parent,
int offset, int fromIndex, int toIndex) {
this.parent = parent;
this.parentOffset = fromIndex;
this.offset = offset + fromIndex;
this.size = toIndex - fromIndex;
this.modCount = ArrayList.this.modCount;
}
可以看到,这个构造函数中把原来的List以及该List中的部分属性直接赋值给自己的一些属性了。
也就是说,SubList并没有重新创建一个List,而是直接引用了原有的List(返回了父类的视图),只是指定了一下他要使用的元素的范围而已(从fromIndex(包含),到toIndex(不包含))。
**总结:**我们简单总结一下,List的subList方法并没有创建一个新的List,而是使用了原List的视图(fromIndex和toIndex),这个视图使用内部类SubList表示。
所以,我们不能把subList方法返回的List强制转换成ArrayList等类,因为他们之间没有继承关系。
另外,视图和原List的修改还需要注意几点,尤其是他们之间的相互影响:
对父(sourceList)子(subList)List做的非结构性修改(non-structural changes),都会影响到彼此。
对子List做结构性修改,操作同样会反映到父List上。
对父List做结构性修改,会抛出异常ConcurrentModificationException。
https://www.hollischuang.com/archives/3775
select version();
show engines
select @@tx_isolation;
8.0版本
select @@transaction_isolation
show engine innodb status
StringJoiner sj = new StringJoiner("Hollis");
sj.add("hollischuang");
sj.add("Java干货");
System.out.println(sj.toString());
StringJoiner sj1 = new StringJoiner(":","[","]");
sj1.add("Hollis").add("hollischuang").add("Java干货");
System.out.println(sj1.toString());
根据此特性,可使用StringJoiner通过一个LIst进行字符串拼接
所谓线程池本质是一个hashSet。多余的任务会放在阻塞队列中。
只有当阻塞队列满了后,才会触发非核心线程的创建。所以非核心线程只是临时过来打杂的。直到空闲了,然后自己关闭了。
线程池提供了两个钩子(beforeExecute,afterExecute)给我们,我们继承线程池,在执行任务前后做一些事情。
线程池原理关键技术:锁(lock,cas)、阻塞队列、hashSet(资源池)
使用ThreadPoolExecutor创建线程池,不允许使用Executors去创建
原理
Java中的BlockingQueue主要有两种实现,分别是ArrayBlockingQueue 和 LinkedBlockingQueue。
ArrayBlockingQueue是一个用数组实现的有界阻塞队列,必须设置容量。
LinkedBlockingQueue是一个用链表实现的有界阻塞队列,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE。
这里的问题就出在:不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE。也就是说,如果我们不设置LinkedBlockingQueue的容量的话,其默认容量将会是Integer.MAX_VALUE。
而newFixedThreadPool中创建LinkedBlockingQueue时,并未指定容量。此时,LinkedBlockingQueue就是一个无边界队列,对于一个无边界队列来说,是可以不断的向队列中加入任务的,这种情况下就有可能因为任务过多而导致内存溢出问题。
创建线程池的正确姿势:
private static ExecutorService executor = new ThreadPoolExecutor(10, 10,
60L, TimeUnit.SECONDS,
new ArrayBlockingQueue(10));
这种情况下,一旦提交的线程数超过当前可用线程数时,就会抛出java.util.concurrent.RejectedExecutionException,这是因为当前线程池使用的队列是有边界队列,队列已经满了便无法继续处理新的请求。但是异常(Exception)总比发生错误(Error)要好。
ThreadLocal
ThreadLocal存放的值是线程内共享的,线程间互斥的,主要用于线程内共享一些数据,避免通过参数来传递,这样处理后,能够优雅的解决一些实际问题。
public class MultiThreads {
public static void main(String[] args) throws InterruptedException {
CallableThread callableThread = new CallableThread();
FutureTask futureTask = new FutureTask<>(callableThread);
new Thread(futureTask).start();
System.out.println(futureTask.get());
}
class CallableThread implements Callable {
@Override
public Object call() throws Exception {
System.out.println(Thread.currentThread().getName());
return "Hollis";
}
}
public class MultiThreads {
public static void main(String[] args) throws InterruptedException, ExecutionException {
System.out.println(Thread.currentThread().getName());
System.out.println("通过线程池创建线程");
ExecutorService executorService = new ThreadPoolExecutor(1, 1, 60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(10));
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
}
}
SimpleDateFormat不是一个线程安全的类。见阿里巴巴编码规范:
如何解决:
for (int i = 0; i < 100; i++) {
//获取当前时间
Calendar calendar = Calendar.getInstance();
int finalI = i;
pool.execute(() -> {
//加锁
synchronized (simpleDateFormat) {
//时间增加
calendar.add(Calendar.DATE, finalI);
//通过simpleDateFormat把时间转换成字符串
String dateString = simpleDateFormat.format(calendar.getTime());
//把字符串放入Set中
dates.add(dateString);
//countDown
countDownLatch.countDown();
}
});
}
/**
* 使用ThreadLocal定义一个全局的SimpleDateFormat
*/
private static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
//用法
String dateString = simpleDateFormatThreadLocal.get().format(calendar.getTime());
ThreadLocal保证每个线程都有一个独享的对象,避免了频繁创建的对象,也避免了多线程的竞争。
使用Slf4j等门面日志而不直接使用Log4j等日志。
有实现类 ClassPathXmlApplicationContext 和 FileSystemXmlApplicationContext Spring提供的两个常用的ApplicationContext实现类,前者默认从类路径加载配置文件,后者从文件系统加载。 然后初始化一个ApplicationContext。
ApplicationContext ac =new ClassPathXmlApplicationContext("application.xml");
ApplicationContext ac =new FileSystemXmlApplicationContext(/User/Desktop/application.xml);
Spring通过BeanPostProcessor处理Autowired注解,所以不能在自己的BeanPostProcessor或BeanFactoryPostProcessor类型应用这些注解,这些类型必须通过xml或者Spring的@Bean注解进行加载。
String是final类型的数据,因此+号拼接需要重新创建String对象。
StringBuilder类似String内部也是char[] 但并不是final类型,因此使用append添加内容时会进行扩容,并添加元素。
StringBuffer内部使用synchronized保证线程安全。
简单的理解内存泄漏的问题就是不使用的对象没有被回收(没有被GC的对象。)
解决方案:
AQS的英文全称为AbstractQueuedSynchronizer 队列同步器。
AQS实现了对同步状态的管理,以及对阻塞线程进行排队,等待通知等等一些底层的实现处理。AQS的核心也包括了这些方面:同步队列,独占式锁的获取和释放,共享锁的获取和释放以及可中断锁,超时等待锁获取这些特性的实现,而这些实际上则是AQS提供出来的模板方法
其含义为当共享资源被某个线程占有,其他请求该资源的线程将会阻塞,从而进入同步队列。(队列有两种实现形式 一种为数组,一种为链表)AQS底层采用双向链表队列的形式进行实现,包含共享锁和独占锁。
借用网上广为流传的一个图。
Spring Bean的声明周期分为对象的实例化,和销毁两个过程。在初始化Spring ApplicationContext后进行Bean的实例化,对Bean的实例化有域注入,setter注入和构造器注入三种形式。
更详细的过程可参考 https://www.zhihu.com/question/38597960
理解数据库的架构需要从他的物理存储和逻辑存储两方面来理解,参照下图:
物理存储指的是我们的文件存储结构,数据库中的数据一定是存储在磁盘上,而我们常说的索引树之类的内容,都是逻辑上的结构。可参照上图进行理解,上图包含了数据库相关的全部架构和知识体系。
不同于Mysql的行式存储结构,列式存储对于 OLTP 不友好,一行数据的写入需要同时修改多个列。但对 OLAP 场景有着很大的优势:
OLTP是传统的关系型数据库的主要应用,主要是基本的、日常的事务处理,记录即时的增、删、改、查,比如在银行存取一笔款,就是一个事务交易。OLAP即联机分析处理,是数据仓库的核心部心,支持复杂的分析操作,侧重决策支持,并且提供直观易懂的查询结果。典型的应用就是复杂的动态报表系统。
内部类总共有四种,先来看一下他们在表层上或者说是写法上的差异:
那么内部类存在的意义究竟?
众所周知,我们的C++设计是多继承的,而多继承的好处就是我们只需要写很少的代码即可完成一个尅的的定义,可以通过集成其他类来获取其他类的实现。
Java在设计继承机制时认为多继承会是一个麻烦,所以Java采用了单继承的方式进行实现,其思想是is-a的原则,(如果一个类是A,那么就不能是B,因此是单继承的设计)。
但是多继承的方式也并不是一无是处,在一些需要大量复用代码的情况下,也不失为一个好的解决方式。因此采用内部类的设计方式我们可以很好的解决大量复用的问题。采用内部类的方式,我们可以很轻松地在一个类的内部定义多个内部类,并分别继承多个类。因此我们可以说Java的内部类完善了他的多继承机制。
通过例子来理解一下:
public interface Contents {
int value();
}
public interface Destination {
String readLabel();
}
public class Goods {
private class Content implements Contents {
private int i = 11;
public int value() {
return i;
}
}
protected class GDestination implements Destination {
private String label;
private GDestination(String whereTo) {
label = whereTo;
}
public String readLabel() {
return label;
}
}
public Destination dest(String s) {
return new GDestination(s);
}
public Contents cont() {
return new Content();
}
}
class TestGoods {
public static void main(String[] args) {
Goods p = new Goods();
Contents c = p.cont();
Destination d = p.dest("Beijing");
}
}
这是最普通的使用方式。
在这个例子里类Content和GDestination被定义在了类Goods内部,并且分别有着protected和private修饰符来控制访问级别。Content代表着Goods的内容,而GDestination代表着Goods的目的地。它们分别实现了两个接口Content和Destination。在后面的main方法里,直接用 Contents c和Destination d进行操作,你甚至连这两个内部类的名字都没有看见!这样,内部类的第一个好处就体现出来了 隐藏你不想让别人知道的操作,也即封装性。
同时,我们也发现了在外部类作用范围之外得到内部类对象的第一个方法,那就是利用其外部类的方法创建并返回。上例中的cont()和dest()方法就是这么做的。那么还有没有别的方法呢?当然有,其语法格式如下:
outerObject=new outerClass(Constructor Parameters);
outerClass.innerClass innerObject=outerObject.new InnerClass(Constructor Parameters);
注意在创建非静态内部类对象时,一定要先创建起相应的外部类对象。至于原因,也就引出了我们下一个话题 非静态内部类。
类对象有着指向其外部类对象的引用 对刚才的例子稍作修改:
public class Goods {
private int valueRate = 2;
private class Content implements Contents {
private int i = 11 * valueRate;
public int value() {
return i;
}
}
protected class GDestination implements Destination {
private String label;
private GDestination(String whereTo) {
label = whereTo;
}
public String readLabel() {
return label;
}
}
public Destination dest(String s) {
return new GDestination(s);
}
public Contents cont() {
return new Content();
}
}
在这里我们给Goods类增加了一个private成员变量valueRate,意义是货物的价值系数,在内部类Content的方法value()计算价值时把它乘上。我们发现,value()可以访问valueRate,这也是内部类的第二个好处 一个内部类对象可以访问创建它的外部类对象的内容,甚至包括私有变量!这是一个非常有用的特性,为我们在设计时提供了更多的思路和捷径。要想实现这个功能,内部类对象就必须有指向外部类对象的引用。Java编译器在创建内部类对象时,隐式的把其外部类对象的引用也传了进去并一直保存着。这样就使得内部类对象始终可以访问其外部类对象,同时这也是为什么在外部类作用范围之外向要创建内部类对象必须先创建其外部类对象的原因。
有人会问,如果内部类里的一个成员变量与外部类的一个成员变量同名,也即外部类的同名成员变量被屏蔽了,怎么办?没事,Java里用如下格式表达外部类的引用:
outerClass.this
有了它,我们就不怕这种屏蔽的情况了。
public class Goods {
private int valueRate = 2;
private class Content implements Contents {
private int i = 11 * valueRate;
public int value() {
return i;
}
}
protected class GDestination implements Destination {
private String label;
private GDestination(String whereTo) {
label = whereTo;
}
public String readLabel() {
return label;
}
}
public Destination dest(String s) {
return new GDestination(s);
}
public Contents cont() {
return new Content();
}
}
这实际上和C++中的嵌套类很相像了,Java内部类与C++嵌套类最大的不同就在于是否有指向外部的引用这一点上,当然从设计的角度以及以它一些细节来讲还有区别。
除此之外,在任何非静态内部类中,都不能有静态数据,静态方法或者又一个静态内部类(内部类的嵌套可以不止一层)。不过静态内部类中却可以拥有这一切。这也算是两者的第二个区别吧。
是的,Java内部类也可以是局部的,它可以定义在一个方法甚至一个代码块之内。
public class Goods1 {
public Destination dest(String s) {
class GDestination implements Destination {
private String label;
private GDestination(String whereTo) {
label = whereTo;
}
public String readLabel() {
return label;
}
}
return new GDestination(s);
}
public static void main(String[] args) {
Goods1 g = new Goods1();
Destination d = g.dest("Beijing");
}
}
java的匿名内部类的语法规则看上去有些古怪,不过如同匿名数组一样,当你只需要创建一个类的对象而且用不上它的名字时,使用内部类可以使代码看上去简洁清楚。它的语法规则是这样的:
new interfacename(){…}; 或 new superclassname(){…};
下面接着前面继续举例子:
public class Goods3 {
public Contents cont() {
return new Contents() {
private int i = 11;
public int value() {
return i;
}
};
}
}
这里方法cont()使用匿名内部类直接返回了一个实现了接口Contents的类的对象,看上去的确十分简洁。
在java的事件处理的匿名适配器中,匿名内部类被大量的使用。例如在想关闭窗口时加上这样一句代码:
frame.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent e){
System.exit(0);
}
});
三次握手 四次挥手
类加载器加载类,首先会把这个类请求委派给父类加载器去进行类加载,每一层都是如此,一直递归到顶层。
存在原因:
双亲委派有啥好处呢?它使得类有了层次的划分。就拿java.lang.Object来说,你加载它经过一层层委托最终是由Bootstrap ClassLoader来加载的,也就是最终都是由Bootstrap ClassLoader去找
这样如果有不法分子自己造了个java.lang.Object,里面嵌了不好的代码,如果我们是按照双亲委派模型来实现的话,最终加载到JVM中的只会是我们rt.jar里面的东西,也就是这些核心的基础类代码得到了保护。
JDBC的SPI机制:
在rt.jar里面定义了SPI。
mysql有mysql的jdbc实现,oracle有oracle的jdbc实现,反正我java不管你内部如何实现的,反正你们都得统一按我这个来,这样我们java开发者才能容易的调用数据库操作。所以因为这样那就不得不违反这个约束啊,Bootstrap ClassLoader就得委托子类来加载数据库厂商们提供的具体实现。因为它的手只能摸到
Java就搞了个线程上下文类加载器,通过setContextClassLoader()默认情况就是应用程序类加载器然后Thread.current.currentThread().getContextClassLoader()获得类加载器来加载。