Java常见知识点汇总(3)

2018-03-01 12:24:17来源:作者:人点击

分享

当前内容来自互联网整理
OutOfMemoryError
在eclipse中设置-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError(堆最小值、最大值设置成一样是为了避免自动扩展,输出内存溢出时信息);
Java堆用于存储对象实例,只要不断创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收,当对象数量达到最大堆容量后就产生内存溢出异常。
final和static
final List<String> list = new ArrayList<>();
list.add("a");
final指向可变的对象,final修饰的方法可以重载但是不能重写;static方法重写后必须是static的,static方法也可以重载;非static不能用static重写。
JVM参数
命令行设置:java -Xms256m -Xmx1024m;
Eclipse和Tomcat都是Java写的程序,故也使用JVM且相当于JVM的一部分,可以通过其设置JVM参数,开两个java程序会启动两个jvm实例;
修改垃圾收集器:-XX:+UseParNewGC(在eclipse或Tomcat修改catalina.bat文件)
查看使用的垃圾收集器:命令行输入:jvisualvm或者打开Java VisualVm查看;
Eclipse中,-XX:+UseG1GC,默认使用G1;
Eclipse默认堆的大小256m到1024m;
Tomcat初始堆大小:128mb(8GB/64)(开启tomcat进入localhost:8080后,点击server status后查看);
JVM初始分配的堆内存由-Xms指定,默认是物理内存的1/64;
JVM最大分配的堆内存由-Xmx指定,默认是物理内存的1/4;
默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;
空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制;
JVM使用-XX:PermSize设置非堆内存初始值,默认是物理内存的1/64;
由XX:MaxPermSize设置最大非堆内存的大小,默认是物理内存的1/4;
JVM内存限制于实际的最大物理内存,假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系;简单的说就32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统下为2G-3G),而64bit以上的处理器就不会有限制了;
Tomcat修改堆大小,修改catalina.bat文件,set JAVA_OPTS=-Xms256m –Xmx512m或打开tomcat8w.exe来根据图形化界面修改;
-Xms :设置Java堆栈的初始化大小,默认物理内存的1/64(<1GB)
-Xmx :设置最大的Java堆大小,默认物理内存的1/4(<1GB)
-Xmn :设置年轻代区大小,整个堆大小=年轻代大小 + 年老代大小 + 持久代大小。增大年轻代后会减小老年代
-Xss :设置Java线程堆栈大小,JDK5.0以后每个线程堆栈大小为1M,以前为256K
-XX:PermSize:设置永久代初始大小,默认物理内存的1/64(<1GB)
-XX:MaxPermSize :设置永久代最大大小,默认物理内存的1/4(<1GB)
-XX:NewRatio :设置年轻代和老年代的比值,默认4
-XX:NewSize :设置年轻代的大小(JDK1.3/1.4)
-XX:SurvivorRatio=n :设置年轻代中eden与Survivor比值,默认8
-XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代. 对于年老代比较多的应用,可以提高效率
-XX:PretenureSizeThreshold参数设置(如3MB),大于这个参数的直接进入老年代
java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0
发现虚拟机频繁GC,扩大堆大小,使用jconsole或jvisualvm查看
ThreadLocal<T>
对于多线程资源共享的问题,同步机制采用了以时间换空间的方式,而ThreadLocal采用了以空间换时间的方式;前者仅提供一份变量来让不同的线程排队访问,而后者为每个线程都提供了一份变量,因此可以同时访问而互不影响。
ThreadLocal 并不能替代同步机制,两者面向的问题领域不同:
1:同步机制是为了同步多个线程对相同资源的并发访问,是为了多个线程之间进行通信的有效方式;
2:而ThreadLocal是隔离多个线程的数据共享,从根本上就不在多个线程之间共享变量,这样当然不需要对多个线程进行同步了,即每个线程有单独的数据,在线程内共享,在线程外独立
最常见的ThreadLocal使用场景为用来解决数据库连接、Session管理等;
JDK中建议ThreadLocal实例通常来说都是private static类型的;
JDK最早期的ThreadLocal设计:每个ThreadLocal类创建一个Map,然后用线程的ID作为Map的key,实例对象作为Map的value,这样就能达到各个线程的值隔离的效果。
ThreadLocal底层实现
ThreadLocal类中存在静态内部类ThreadLocalMap,ThreadLocalMap相当于哈希表,用private Entry[] table来存储数据,而Entry是实现了弱引用(下次GC时就会被回收)的内部类,它的Key是弱引用;每个线程内部都有ThreadLocalMap类型的threadLocals。
ThreadLocalMap的初始大小为16,负载因子为2/3(即超过了长度的三分之二就要扩容),每次扩容为原来的2倍,可以保证大小始终为2的N次方。ThreadLocalMap解决哈希冲突的方法与HashMap不同(数组+链表),ThreadLocalMap如果i位置已经存储了对象,那么就往后挪一个位置依次类推,直到找到空的位置后再将对象存放。另外在最后还需要判断一下当前的存储的对象个数是否已经超出了阈值(threshold的值)大小,如果超出了则需要重新扩充并将所有的对象重新计算位置(rehash函数来实现)。Rehash函数里面先调用了expungeStaleEntries函数,然后再判断当前存储对象的大小是否超出了阈值的3/4。如果超出了则再扩容。
为什么不直接扩容并重新摆放对象?为啥要搞成这么复杂?
ThreadLocalMap里面存储的Entry对象本质上是WeakReference<ThreadLocal>,里面存储的对象本质是对ThreadLocal对象的弱引用,该ThreadLocal随时可能会被回收,即导致ThreadLocalMap里面对应的Value的Key是null,需要把这样的Entry给清除掉,不要让它们占坑,expungeStaleEntries函数就是实现这样的清理工作,清理完后实际存储的对象数量自然会减少,这时候再判断,如果存储对象数量还是过多,才会扩容(resize)。
ThreadLocalMap中根据key值获得entry对象的方法是,得到table中的位置i(根据len-1,低位掩码),如果没找到则有可能发生哈希冲突,所以调用getEntryAfterMiss函数从当前位置继续向后找。
threadLocalHashCode方法就是在ThreadLocal中定义了一个static的atomicInteger,每次调用threadLocalHashCode方法都要给它加上固定的值【这么处理的缘由没弄清楚】。
在调用ThreadLocal类的get方法时,先获得当前线程中存储的threadLocals(是个map),如果该map为空,则调用setInitialValue(给当前线程的map new个ThreadLocalMap, 传入initialValue方法的初值(没重写的话为null)),并返回该初值;如果有map的话,就map.getEntry(this),注意这里的key是this,也就是该ThreadLocal类。
因为set的key是this(ThreadLocal类),所以每个线程在每个ThreadLocal中只能对应一个value。若想保存多个value,则需要创建多个ThreadLocal类。
总结:
Thread内部封装了ThreadLocalMap,这个map的key是ThreadLocal(map.getEntry(this)),value是具体的变量对象,每个thread可以保存多个threadlocal,因为threadlocal保存在thread内部,故多线程并发时每次处理的都是自己内部的数据。
流程:
新建ThreadLocal类(名叫tl),重写它的initialValue方法,当一个线程调用tl的get方法时(此时进入tl类内部),先获得调用get方法线程保存的map,用map.getEntry(this)获得对应的变量对象(因为调用的是tl的get方法,所以this指针为这个tl)。
与早期JDK中设计的区别
1.每个Map的Entry数量变小了,之前是Thread的数量,现在是ThreadLocal的数量,能提高性能
2.当Thread销毁之后对应的ThreadLocalMap也就随之销毁了,能减少内存使用量。
弱引用
Threadlocal里面使用了存在弱引用的map,当释放掉threadlocal的强引用以后,map里面的value却没有被回收且永远不会被访问到,故存在着内存泄露,最好的做法是将调用threadlocal的remove方法。
ThreadLocal<String> tl = new ThreadLocal<String>();当tl=null时释放了强引用,此时这个ThreadLocal会被GC掉,每个线程中ThreadLocalMap的key(key为threadlocal),如果key为强引用,则这个ThreadLocal不会被GC,就会发生不想要这个ThreadLocal,但还没被GC,所以为弱引用,value是threadlocal中initial出来的。
当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被GC回收. 但是value不能回收,因为存在一条从current thread连接过来的强引用,只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收。
只要这个线程对象被GC回收就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间不会被回收的,就发生了认为的内存泄露。线程对象不被回收,这就发生了真正意义上的内存泄露,比如使用线程池的时候,线程结束是不会销毁的,会再次使用的,就可能出现内存泄露。
Java为了最小化减少内存泄露的可能性和影响,在ThreadLocal的get,set的时候都会清除线程Map里所有key为null的value,threadLocal对象设null了,开始发生“内存泄露”,然后使用线程池,这个线程结束,线程放回线程池中不销毁,这个线程一直不被使用,或者分配使用了又不再调用get,set方法,那么这个期间就会发生真正的内存泄露。
equals
Equals与==的区别,==是比较值,equals是比较内容;Equals是object类的方法,是比较两者地址是否相同(用==号);String等类重写了这个方法,也重写了hashcode方法,如果地址相同直接返回true,如果地址不同则每位比较char是否相同;
equals()和hashCode()方法是用来在同一类中做比较用的,尤其是在容器里如set存放同一类对象时用来判断放入的对象是否重复;equals()方法不相等的两个对象,hashCode()有可能相等(哈希码冲突造成?);
当重写对象的equals方法时,必须重写hashCode方法,不重写hashCode方法的话,Object对象中的hashCode方法始终返回的是一个对象的hash地址,而这个地址是永远不相等的。所以这时即使是重写了equals方法,也不会有特定的效果的,因为hashCode方法如果都不相等的话,就不会调用equals方法进行比较。
如果类的hashCode()方法没有遵循上述要求,那么当这个类的两个实例对象用equals()方法比较的结果相等时,它们本来应该无法被同时存储进set集合中,但是如果将它们存储进HashSet集合中时,由于它们的hashCode()方法的返回值不同(Object中的hashCode方法返回值是永远不同的),第二个对象首先按照哈希码计算可能被放进与第一个对象不同的区域中,这样就不可能与第一个对象进行equals方法比较了,也就可能被存储进HashSet集合中了。
Object类中的hashCode()方法不能满足对象被存入到HashSet中的要求,因为它的返回值是通过对象的内存地址推算出来的,同个对象在程序运行期间的任何时候返回的哈希值都是始终不变的,所以只要是两个不同的实例对象,即使它们的equals方法比较结果相等,它们默认的hashCode方法的返回值是不同的。
基本数据类型存放
class demo{
private int a = 3;
private void fun(){
int b = 4;
}
}
b存放在栈中,a随实例对象存放在堆中;
Java的8种基本类型(Byte, Short, Integer, Long, Character, Boolean, Float, Double), 除Float和Double以外, 其它六种都实现了常量池, 但是它们只在大于等于-128并且小于等于127时才使用常量池(2的7次方,字节)。
Java中二维数组的存放
如果a是局部变量,则a存储在栈中,而数组存储在堆中,可以定义列数不相同的数组,二维数组的第一维“行”,存储了第二维的数组,相当于存储了数组的数组,第一维中是第二维中的地址。
反射
反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制;如在运行状态中(正在运行的服务器,不用关了服务器再修改),修改配置文件,切换不同的数据库。
不用类名也可以获得class对象,this.getClass()方法,this不能在static方法中使用。
在Java虚拟机规范中,必须开始初始化的情况之一就是调用Class.forName();而加载、验证、准备必须在初始化之前开始;调用Class.forName(“xxx”)后就开始初始化;准备阶段会给static类变量赋值0,初始化阶段会赋值设置的值。
Class.forName()会调用forName0方法,使用默认的类加载器,而forName0是native方法。native关键字说明其修饰的方法是原生态方法,方法对应的实现不是在当前文件,而是在用其它语言(如C和C++)实现的文件中。
(a)Class.forName()必须开始初始化
(b)可以在Class.forName()传入类加载器classLoader,findClass和loadClass方法,重写loadClass可以破坏双亲委派模型。
(c)调用Class.forName后,完成类的加载过程。即该类被加载进了内存。每个类都有一个Class对象,存放在方法区中。
(d)Class clazz = Class.forName(“aaa”);得到Class类型的对象后,可以生成实例,并调用方法。
反射用途
当在使用IDE(如Eclipse,IDEA)时,当输入对象或类并想调用它的属性或方法时,一按点号,编译器就会自动列出它的属性或方法,这里就会用到反射。很多框架(比如Spring)都是配置化的(比如通过XML文件配置JavaBean,Action之类的),为了保证框架的通用性,它们可能需要根据配置文件加载不同的对象或类,调用不同的方法,这个时候就必须用到反射:运行时动态加载需要加载的对象。
反射能够使用私有的方法属性
可以访问私有 只能访问公有
Class.getDeclaredField(String name) Class.getField(String name)
Class.getDeclaredFields() Class.getFields()
getDeclaredMethod((String name,
Class<?>... parameterTypes)) getMethod((String name,
Class<?>... parameterTypes))
getDeclaredMethods() getMethods()
访问私有字段,需要调用Declared方法;method.setAccessible(true)来设置访问权限,如果不设置会抛异常。
Declared方法不可以获得继承的方法(公有继承也不行),当然也包括它所实现接口的方法,不加Declared可以获得公有的继承方法和实现接口的方法(包括Object中的方法)。通过反射能够获得方法Method的形参类型,返回值类型,方法名称,修饰符,注解。Field可以获得修饰符,注解,字段的类型,字段的值。
通过getClass()方法来获取类的定义信息,通过定义信息再调用getFields()方法来获取类的所有公共属性(只有公有,可以获得父类属性),或者调用getDeclaredFields()方法来获取类的所有属性,包括公共,保护,私有,默认的方法,但是这里要注意的是这个方法只能获取当前类里面显示定义的属性,不能获取到父类或者父类的父类及更高层次的属性的,可以使用Class.getSuperclass()得到父类,再获取其字段。
没有多继承
多继承容易引起混乱,比如相同的方法在两个父类中都有,子类到底需要继承哪个会很混乱。
Java是通过实现多个接口来弥补不能多继承的缺陷的,接口中的方法都是抽象的,子类可以选择实现。
Java的注解
注解:元数据,描述数据的数据,源代码的元数据。注解Annotations仅仅是元数据,和业务逻辑无关,业务逻辑可以通过反射,获得注解中的数据
元注解(描述注解的注解)
@Documented:简单的Annotations标记注解,表示是否将注解信息添加在Java文档中
@Retention:定义该注解的生命周期
RetentionPolicy.SOURCE:在编译阶段丢弃,在编译结束后不再有任何意义,不写入字节码;@Override, @SuppressWarnings都属于RetentionPolicy.SOURCE这类注解。
RetentionPolicy.CLASS:在类加载的时候丢弃,在字节码文件的处理中有用,注解默认;
RetentionPolicy.RUNTIME:始终不会丢弃,运行期也保留该注解,因此可以使用反射机制读取该注解的信息,自定义的注解通常使用这种方式。
@Target:表示该注解用于什么地方,如果不明确指出,该注解可以放在任何地方,以下是一些可用的参数:
ElementType.CONSTRUCTOR(构造方法声明),
FIELD(字段声明),
LOCAL VARIABLE(局部变量声明),
METHOD(方法声明),
PACKAGE(包声明),
PARAMETER(参数声明),
TYPE(类接口),
ANNOTATION_TYPE (另一个注释,元注解中用到);
@Inherited – 定义该注释和子类的关系
Annotations只支持基本类型、String、枚举、class类型;注释中所有的属性被定义成类似方法的样子,并允许提供默认值;method或field都有getAnnotation方法,可以获得method或field上的注解。
JDK与JRE
JRE是java runtime environment,是Java程序的运行环境;既然是运行,当然要包含JVM,还有所有Java类库的class文件(包括java.lang包、java.util包等),都在lib目录下打包成了jar,Java命令属于JRE。
JDK(java development kit)主要包含三部分,
(a)Java运行时环境JVM
(b)Java的基础类库
(c) Java的开发工具
Proxy
代理位于java.lang.reflect包中,分为静态代理和动态代理,静态代理是在编译时就将接口、实现类、代理类全部手动完成,动态代理可以在程序运行期间根据需要动态的创建代理类及其实例来完成具体的功能。
Proxy.newProxyInstance()方法利用参数中的classLoader生成字节码文件并动态生成代理类$ProxyN,这个类继承了Proxy并且实现了传入的一组接口,循环遍历传入接口中的每一个方法,将方法设置到$ProxyN中,父类Proxy中有InvocationHandler h属性,利用反射获得传入接口中的方法method,将method作为参数传入h中,调用h的invoke方法利用反射完成代理。
Proxy 类是它的父类,这个规则适用于所有由 Proxy 创建的动态代理类,这算是Java动态代理的缺陷,Java不支持多继承,所以无法实现对class的动态代理,只能对于Interface的代理,而且该类还实现了其所代理的一组接口,这就是为什么它能够被安全地类型转换到其所代理的某接口的根本原因。
传入一组接口的原因:
1、面向接口编程,一种规范;
2、Java不支持多继承,如果一个类实现了一组接口,对其进行代理时,不同接口的不同方法都会得到增强。
一个典型的动态代理创建对象过程可分为以下四个步骤:
1、通过实现InvocationHandler接口创建自己的调用处理器 IvocationHandler handler = new InvocationHandlerImpl(...);
2、通过为Proxy类指定ClassLoader对象和一组interface创建动态代理类
Class clazz = Proxy.getProxyClass(classLoader,new Class[]{...});
3、通过反射机制获取动态代理类的构造函数,其参数类型是调用处理器接口类型
Constructor constructor =
clazz.getConstructor(new Class[]{InvocationHandler.class});
4、通过构造函数创建代理类实例,此时需将调用处理器对象作为参数被传入
Interface Proxy = (Interface)constructor.newInstance(new Object[] (handler));
为了简化对象创建过程,Proxy类中的newInstance方法封装了2~4,只需两步即可完成代理对象的创建。
CGLIB动态代理
JDK的动态代理机制只能代理实现了接口的类,而不能实现接口的类就不能实现JDK的动态代理,CGLIB是针对类来实现代理的,它的原理是对指定的目标类生成一个子类,并覆盖其中方法实现增强,但因为采用的是继承,所以不能对final修饰的类进行代理。
迭代遍历的几种方法
(a)使用lambda表达式,list.forEach(i->{sum+=i;}),sum为静态成员变量
(b)for循环,前置的自增运算符(++i)速度快,因为前置的自增后直接返回引用,而后置的先定义一个临时变量,把值赋给临时变量,然后自增,返回临时变量
(c)增强的for循环,forEach迭代,for(String str : strings);
对特定位置的元素执行特殊操作时循环变量i的使用才有意义,增强的for循环速度快?
String、StringBuilder、StringBuffer
底层都是使用char类型的数组保存元素value;
StringBuffer是线程安全的,使用synchronized修饰方法来实现;
String是final类型的,也是线程安全的;
速度StringBuilder>StringBuffer>String
String 类型和StringBuffer类型的性能区别在于String是不可变的对象, 因此每次对String类型进行改变的时候其实都等同于生成新的String对象,然后将指针指向新的String对象,所以经常改变内容的字符串最好不要用String,因为每次生成对象都会对系统性能产生影响,特别当内存中无引用对象多了以后,JVM的GC就会开始工作,那速度是一定会相当慢的。
使用StringBuffer,每次结果都会对StringBuffer对象本身进行操作(对char数组操作),而不是生成新的对象再改变对象引用,推荐使用StringBuffer,特别是字符串对象经常改变的情况下。在某些特别情况下String对象的字符串拼接其实是被JVM解释成了 StringBuffer对象的拼接,所以String对象的速度并不会比StringBuffer对象慢。
StringBuffer扩容,利用arrays.copyof重新复制数组,arrays.copyof底层先新建一个长度的数组,再用system.arraycopy复制过来。StringBuilder和StringBuffer的父类都是AbstractStringBuilder,两者的append方法都是使用父类的append方法,append方法中有扩容,所以两者的扩容机制也一样。append也是使用system.arraycopy(native方法)复制数组。
Arrays.sort
Java Arrays中提供了对所有类型的排序,主要分为Primitive(8种基本类型)和Object两大类;基本类型采用调优的快速排序;对象类型采用改进的归并排序(Timsort);
sort(T[] a, Comparator<? super T> c)也是采用归并(Timsort);String数组也是当做object传入;
Java对Primitive(int,float等原型数据)数组采用快速排序,对Object对象数组采用归并排序,优化的归并排序既快速(nlog(n))又稳定。对于对象的排序,稳定性很重要,快速排序是不稳定的,而且最坏情况下的时间复杂度是O(n^2)。
对象数组中保存的只是对象的引用,这样多次移位并不会造成额外的开销,但是对象数组对比较次数一般比较敏感,有可能对象的比较比单纯数的比较开销大很多;归并排序在这方面比快速排序做得更好,这也是选择它作为对象排序的一个重要原因之一。
排序优化:实现中快排和归并都采用递归方式,而在递归的底层,也就是待排序的数组长度小于7时,直接使用冒泡排序,而不再递归下去。
源码中的快速排序,主要做了以下几个方面的优化:
 1)当待排序的数组中的元素个数较少时,源码中的阀值为7,采用的是插入排序。尽管插入排序的时间复杂度为0(n^2),但是当数组元素较少时,插入排序优于快速排序,因为这时快速排序的递归操作影响性能。
 2)较好的选择了划分元(基准元素)。能够将数组分成大致两个相等的部分,避免出现最坏的情况。例如当数组有序的的情况下,选择第一个元素作为划分元,将使得算法的时间复杂度达到O(n^2).
源码中选择划分元的方法:
 当数组大小为 size=7 时 ,取数组中间元素作为划分元。int n=m>>1;
 当数组大小 7<size<=40时,取首、中、末三个元素中间大小的元素作为划分元。
 当数组大小 size>40 时 ,从待排数组中较均匀的选择9个元素,选出一个伪中数做为划分元。
Arrays.copyOf
先对需要copy的数组类型进行判断,然后调用native方法system.arraycopy。
对象数组
A[] a = new A[5];//不加小括号
a[0] = new A();//这里再创建对象
Exeception
Throwable是所有Java程序中错误处理的父类,有两种子类:Error和Exception。
Error:表示由JVM所侦测到的无法预期的错误,由于这是属于JVM层次的严重错误,导致JVM无法继续执行,因此这是不可捕捉到的,无法采取任何恢复的操作,顶多只能显示错误信息。比如:内存资源不足(outofmemoryerror、stackoverflowerror)等。对于这种错误,程序基本无能为力,除了退出运行外别无选择,它是由Java虚拟机抛出的。
Exception:表示可恢复的例外,这是可捕捉到的。
Java提供了两类主要的异常:runtime exception和checked exception。checked 异常: IOException,FileNotFoundException,SQLException,InterruptedException(线程sleep、wait)等,对于这种异常JAVA编译器强制要求必需对出现的这些异常进行catch。runtime exception可以不处理,当出现这样的异常时,总是由虚拟机接管。
出现运行时异常后,系统会把异常往上层抛,直到遇到处理代码;如果没有处理块则到最上层;如果是多线程就由Thread.run()抛出,如果是单线程就被main()抛出。抛出之后,如果是线程则这个线程也就退出了;如果是主程序抛出的异常,那么这整个程序也就退出了。运行时异常是Exception的子类,也有一般异常的特点,是可以被Catch块处理的。
队列里面出现异常数据了,正常的处理应该是把异常数据舍弃,然后记录日志;不应该由于异常数据而影响下面对正常数据的处理。异常处理的目标之一就是为了把程序从异常中恢复出来;
常见runtimeException:
NullPointerException - 空指针引用异常
ClassCastException - 类型强制转换异常。
IllegalArgumentException - 传递非法参数异常。
ArithmeticException - 算术运算异常,除数为0
ArrayStoreException - 向数组中存放与声明类型不兼容对象异常
IndexOutOfBoundsException - 下标越界异常
NegativeArraySizeException - 创建一个大小为负数的数组错误异常
NumberFormatException - 数字格式异常
SecurityException - 安全异常
UnsupportedOperationException - 不支持的操作异常
如何抛出异常
① 语法:throw
② 对于异常对象,真正有用的信息是异常的对象类型,而异常对象本身毫无意义
③ 异常对象通常有两种构造函数:无参数的构造函数;带字符串的构造函数,这个字符串将作为这个异常对象除了类型名以外的额外说明。
④ 创建自己的异常
finally的用法与异常处理流程:
异常处理并不需要回收内存,但是依然有一些资源需要程序员来收集,比如文件、网络连接和图片等资源。捕捉并处理哪些知道如何处理的异常,而传递哪些不知道如何处理的异常。
for循环
在for循环里面定义的变量,不能在外面获得值,这是for循环的作用域;
循环内的话,每次循环内部的局部变量在每次进for循环的时候都要重新定义一遍变量(栈中),也就是执行申请内存空间,变量压入堆栈的过程;
循环外定义的话,for循环一直用的是同一块内存空间,效率比较高,但是变量的作用域就大了,耗内存。
Java内存泄漏
内存泄露是指不再使用的对象持续占有内存或无用对象的内存得不到及时释放,从而造成的内存空间的浪费称为内存泄露,无法被GC。
长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是Java中内存泄露的发生场景。
1、静态集合类引起内存泄露:
像HashMap、Vector(被定义为static)等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,它们所引用的所有的对象Object也不能被释放,因为它们也将一直被Vector等引用着。
2、当集合里面的对象属性被修改后,再调用remove()方法时不起作用。
3、监听器
往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。
4、各种连接
除非其显式的调用了其close()方法将其连接关闭,否则是不会自动被GC 回收的。
每个线程中ThreadlocalMap的key为弱引用的Threadlocal对象,value为初始化值,Threadlocal的强引用被释放后,弱引用也会释放,但value有一条从当前线程的引用,只有当前线程结束后才会释放,如果使用连接池,不释放线程,会导致泄漏;
内存溢出
java.lang.OutOfMemoryError内存不够用
1) 内存泄露是导致内存溢出的原因之一;内存泄露积累起来将导致内存溢出。
2) 内存泄露可以通过完善代码来避免;内存溢出可以通过调整配置来减少发生频率,但无法彻底避免。
永久代溢出:OutOfMemoryError:PermGen space
增加java虚拟机中的XX:PermSize和XX:MaxPermSize参数的大小,其中XX:PermSize是初始永久保存区域大小,XX:MaxPermSize是最大永久保存区域大小。
堆溢出:OutOfMemoryError:Java heap space
1. 检查程序,看是否有死循环或不必要地重复创建大量对象。找到原因后修改程序和算法。
2. 增加Java虚拟机中Xms(初始堆大小)和Xmx(最大堆大小)参数的大小。如:set JAVA_OPTS= -Xms256m -Xmx1024m
栈溢出:
1.StackOverflowError(方法调用层次太深,内存不够新建栈帧)
2.OutOfMemoryError(线程太多,内存不够新建线程)
Java多态
多态存在的三个必要条件(屏蔽子类对象之间的差异,提高代码的扩展性)
要有继承;要有重写;父类引用指向子类对象。
面向对象的三个基本特征:封装、继承、多态
Java泛型
1、 泛型程序设计意味着编写的代码可以被很多不同类型的对象所重用(如ArrayList可以传入String、Integer等多种类型)。
2、 ArrayList<String>具有更好的可读性。
3、 编译器可以进行检查,避免插入错误类型的对象,编译器自动、隐式进行类型转换
4、 没有泛型前,要做显式强制类型转换,容易造成运行时异常,安全隐患。
总结:可以提高代码重用率,使程序具有更好的可读性和安全性。
实现原理:类型擦除
Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除。(如List<String>编译后变成List,JVM只看到List)。
类型擦除后保留的原始类型(字节码中):
1、 无限定的用Object(如public class A<T>,类中的T会用Object替代)
2、 有限定的用限定类型(如public class A<T extends B>,类中的T会用B替代)
假设参数类型的占位符为T,擦除规则如下:
<T>擦除后变为Obecjt
<? extends A>擦除后变为A
<? super A>擦除后变为Object
上述擦除规则叫做保留上界。
重写与重载
1、重写发生在父类与子类之间,重写必须有相同的方法名,参数列表和返回类型。重写的方法作用域必须大于等于原方法。重写方法不能抛出新的异常或者比被重写方法声明的检查异常更广的检查异常。但是可以抛出更少,更有限或者不抛出异常。(原来调用父类的地方,再调用子类抛出异常不能更大)静态方法可以重写和重载(都是static的)。
例子: 在父类中是public的方法,如果子类中将其降低访问权限为private, 那么父类在实现多态时如果调用子类的这个重写方法,但是这个方法已经是private,没有办法调用,所以就无法实现多态了。
2、方法重载发生在同一个类里面两个或者是多个方法的方法名相同但是参数不同的情况。重载方法参数必须不同,返回值可相同可不同,作用域可相同可不同,参数相同返回值不同也不行,参数相同作用域不同也不行;
静态绑定:
在程序执行前方法已经被绑定,此时由编译器或其它连接程序实现,例如:C。
针对Java简单的可以理解为程序编译期的绑定;java当中的方法只有final,static,private和构造方法是前期绑定(private只有自己调用,静态方法与类相关)。
方法重载有静态方法重载和普通方法重载。静态方法重载是静态绑定,方法调用是通过:类名.方法。普通方法重载是动态绑定,方法调用是通过:实例对象引用.方法。构造器能够重载,但是不能够被重写。
静态方法能够被重写、重载,但是没有实现多态效果。
接口与抽象类
Java提供和支持创建抽象类和接口。它们的实现有共同点,不同点在于:
接口中所有的方法隐含的都是抽象的。而抽象类则可以同时包含抽象和非抽象的方法。
类可以实现很多个接口,但是只能继承一个抽象类
类可以不实现抽象类和接口声明的所有方法,当然在这种情况下,类也必须得声明成是抽象的。
抽象类可以在不提供接口方法实现的情况下实现接口。
Java接口中声明的变量默认、必须都是public static final。抽象类可以包含非final的变量。
Java接口中的成员函数默认、必须是public abstract的。抽象类的成员函数可以是private,protected或者是public。
接口是绝对抽象的,不可以被实例化。抽象类也不可以被实例化,但是,如果它包含main方法的话是可以被调用的。
JDK8中default和static;
枚举类可以实现接口,但不能继承类,因为所有枚举类都继承自java.lang.Enum(由编译器添加),同时Java不支持多继承(已经自动继承了一个)。枚举类也无法被继承。枚举类中可以有自己的构造函数,可以实现接口的方法。
遍历
遍历list
超级for循环遍历,for(String attribute : list)
对于ArrayList来说速度比较快, 用for循环, 以size为条件遍历:
集合类的通用遍历方式,用迭代器迭代,Iterator it = list.iterator();
遍历set
1.迭代遍历:Iterator<String> it = set.iterator();
2.for循环遍历:
遍历map
普遍使用,通过Map.keySet遍历key和value;
通过Map.entrySet使用iterator遍历key和value;
Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
通过Map.entrySet遍历key和value,Map.Entry<String, String> entry : map.entrySet());
通过Map.values()遍历所有的value,但不能遍历key;

最新文章

123

最新摄影

闪念基因

微信扫一扫

第七城市微信公众平台