java基础:
- JDK,JRE:
JDK是面向开发人员使用的SDK。java开发工具。
JRE是Java Runtime Enviroment是指Java的运行环境,是面向Java程序的使用者。基础类库。
什么时候装JDK,什么时候JRE?
当你安装 JDK 完成后已经默认安装了 JRE ,因为安装完 JDK 肯定是要开发的吧,开发就要运行 Java 程序,所以 JDK 包含了 Java 的运行环境 JRE。如果你只是单纯的要运行 Java 程序而不进行开发、调试等,那么就只需要安装 JRE 就行了。
服务器上是否只安装JRE就可以了?
理论上是可以的,但是有前提条件。
服务器上只安装JRE的前提:
• 发布到服务器上时所有文件都是编译好的文件,包括JSP文件
• 后期不在服务器上直接修改(因为导致修改后的文件未重新编译)
如果部署的项目都是编译后重新部署,不在服务器上直接修改的话是可以只安装 JRE 的。
所以上文提到的问题,对于开发人员来说安装完JDK后就不需要再安装JRE了。
JDK里重要工具:
javac –
编译器,将源程序转成字节码
jdb –
debugger,查错工具
jcmd
JVM诊断命令工具,将诊断命令请求发送到正在运行的Java虚拟机。
jconsole
用于监控Java虚拟机的使用JMX规范的图形工具。它可以监控本地和远程JVM。它还可以监控和管理应用程序。
jmc
Java任务控制客户端(JMC,Java Mission Control),包含用于监控和管理Java应用程序的工具,而不会引入与这些工具相关联的性能开销。开发者可以使用jmc命令来创建JMC工具。
jvisualvm
一种图形化工具,可在Java虚拟机中运行时提供有关基于Java技术的应用程序(Java应用程序)的详细信息。 Java VisualVM提供内存和CPU分析,堆转储分析,内存泄漏检测,MBean访问和垃圾收集。
jps
JVM进程状态工具(JVM Process Status Tool),在目标系统上列出HotSpot Java虚拟机进程的描述信息
jstat
JVM统计监控工具(JVM Statistics Monitoring Tool),根据参数指定的方式收集和记录指定的jvm进程的性能统计信息。
jstatd
JVM jstat守护程序,启动一个RMI服务器应用程序,用于监视测试的HotSpot Java虚拟机的创建和终止,并提供一个界面,允许远程监控工具附加到在本地系统上运行的Java虚拟机。
jinfo
Java的配置信息工具(Java Configuration Information),用于打印指定Java进程、核心文件或远程调试服务器的配置信息。
jhat
Java堆分析工具(Java Heap Analysis Tool),用于分析Java堆内存中的对象信息。
jmap
Java内存映射工具(Java Memory Map),主要用于打印指定Java进程、核心文件或远程调试服务器的共享对象内存映射或堆内存细节。
jsadebugd
适用于Java的可维护性代理调试守护程序(Java Serviceability Agent Debug Daemon),主要用于附加到指定的Java进程、核心文件,或充当一个调试服务器。
jstack
Java的堆栈跟踪工具,主要用于打印指定Java进程、核心文件或远程调试服务器的Java线程的堆栈跟踪信息。
- JDK8新特性:
一、Lambda表达式
Lambda 表达式也可称为闭包,是推动 Java 8 发布的最重要新特性。lambda表达式本质上是一个匿名方法。Lambda允许把函数作为一个方法的参数(函数作为参数传递进方法中)或者把代码看成数据。使用 Lambda 表达式可以使代码变的更加简洁紧凑。在最简单的形式中,一个lambda可以由:用逗号分隔的参数列表、–>符号、函数体三部分表示,在某些情况下lambda的函数体会更加复杂,这时可以把函数体放到在一对花括号中,就像在Java中定义普通函数一样。Lambda可以引用类的成员变量与局部变量(如果这些变量不是final的话,它们会被隐含的转为final,这样效率更高)。Lambda可能会返回一个值。返回值的类型也是由编译器推测出来的。如果lambda的函数体只有一行的话,那么没有必要显式使用return语句。
java 8之前:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Before Java8, too much code for too little to do");
}
}).start();
Java 8方式:
new Thread( () ->
System.out.println("In Java8, Lambda expression rocks !!") ).start();
Lambda 表达式免去了使用匿名方法的麻烦,并且给予Java简单但是强大的函数化的编程能力。
二、新的日期API
Java 8通过发布新的Date-Time API (JSR 310)来进一步加强对日期与时间的处理。
三、注解相关
(1)可以进行重复注解
(2)扩展注解的支持
四、接口增强
Java 8 对接口做了进一步的增强。在接口中可以添加使用 default 关键字修饰的非抽象方法。还可以在接口中定义静态方法。如今,接口看上去与抽象类的功能越来越类似了。
Java 8 还允许我们给接口添加一个非抽象的方法实现,只需要使用 default 关键字即可,这个特征又叫做扩展方法。在实现该接口时,该默认扩展方法在子类上可以直接使用,它的使用方式类似于抽象类中非抽象成员方法。但扩展方法不能够重载 Object 中的方法。例如:toString、equals、 hashCode 不能在接口中被重载。
例如,下面接口中定义了一个默认方法 count(),该方法可以在子类中直接使用。
public interface DefaultFunInterface {
//定义默认方法 count
default int count(){
return 1;
}
}
public class SubDefaultFunClass implements DefaultFunInterface {
public static void main(String[] args){
//实例化一个子类对象,改子类对象可以直接调用父接口中的默认方法 count
SubDefaultFunClass sub = new SubDefaultFunClass();
sub.count();
}
}
五、Optional
Java应用中最常见的bug就是空值异常。在Java 8之前,Google Guava引入了Optionals类来解决NullPointerException,从而避免源码被各种null检查污染,以便开发者写出更加整洁的代码。Java 8也将Optional加入了官方库。
Optional仅仅是一个容易:存放T类型的值或者null。它提供了一些有用的接口来避免显式的null检查,可以参考Java 8官方文档了解更多细节。
Optional< String > fullName = Optional.ofNullable( null );
如果Optional实例持有一个非空值,则isPresent()方法返回true,否则返回false;orElseGet()方法,Optional实例持有null,则可以接受一个lambda表达式生成的默认值;map()方法可以将现有的Opetional实例的值转换成新的值;orElse()方法与orElseGet()方法类似,但是在持有null的时候返回传入的默认值。
六、Streams
新增的Stream API(java.util.stream)将生成环境的函数式编程引入了Java库中。这是目前为止最大的一次对Java库的完善,以便开发者能够写出更加有效、更加简洁和紧凑的代码。
final long totalPointsOfOpenTasks = tasks
.stream()
.filter( task -> task.getStatus() == Status.OPEN )
.mapToInt( Task::getPoints )
.sum();
Steam之上的操作可分为中间操作和晚期操作。
中间操作会返回一个新的steam——执行一个中间操作(例如filter)并不会执行实际的过滤操作,而是创建一个新的steam,并将原steam中符合条件的元素放入新创建的steam。
晚期操作(例如forEach或者sum),会遍历steam并得出结果或者附带结果;在执行晚期操作之后,steam处理线已经处理完毕,就不能使用了。在几乎所有情况下,晚期操作都是立刻对steam进行遍历。
- 面向过程与面向对象:
面向过程(Procedure Oriented 简称PO :如C语言):
从名字可以看出它是注重过程的。当解决一个问题的时候,面向过程会把事情拆分成: 一个个函数和数据(用于方法的参数) 。然后按照一定的顺序,执行完这些方法(每个方法看作一个过程),等方法执行完了,事情就搞定了。
面向对象(Object Oriented简称OO :如C++,JAVA等语言):
看名字它是注重对象的。当解决一个问题的时候,面向对象会把事物抽象成对象的概念,就是说这个问题里面有哪些对象,然后给对象赋一些属性和方法,然后让每个对象去执行自己的方法,问题得到解决。
面向过程
优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、 Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
缺点:没有面向对象易维护、易复用、易扩展
面向对象
优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护
缺点:性能比面向过程低
三大基本特性:封装,继承,多态
封装
封装,就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体。在一个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。通过这种方式,对象对内部数据提供了不同级别的保护,以防止程序中无关的部分意外的改变或错误的使用了对象的私有部分。
封装隐藏了类的内部实现机制,可以在不影响使用的情况下改变类的内部结构,同时也保护了数据。对外界而已它的内部细节是隐藏的,暴露给外界的只是它的访问方法。
继承
继承,指可以让某个类型的对象获得另一个类型的对象的属性的方法。它支持按级分类的概念。继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。 通过继承创建的新类称为“子类”或“派生类”,被继承的类称为“基类”、“父类”或“超类”。继承的过程,就是从一般到特殊的过程。
注意:继承与接口实现的区别:
继承是指直接使用父类的属性和方法而无需额外编码的能力;接口是指仅使用属性和方法的名称、但是子类必须提供实现的能力。
继承是为了重用父类代码。两个类若存在IS-A的关系就可以使用继承。同时继承也为实现多态做了铺垫。
继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。
继承的特性:
1 子类拥有父类非 private 的属性、方法。
2 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展。
3 子类可以用自己的方式实现父类的方法。
4 Java 的继承是单继承,但是可以多重继承,单继承就是一个子类只能继承一个父类,多重继承就是,例如 A 类继承 B 类,B 类继承 C 类,所以按照关系就是 C 类是 B 类的父类,B 类是 A 类的父类,这是 Java 继承区别于 C++ 继承的一个特性。
5 提高了类之间的耦合性(继承的缺点,耦合度高就会造成代码之间的联系越紧密,代码独立性越差)。
所有的类都是继承于 java.lang.Object,当一个类没有继承的两个关键字,则默认继承object(这个类在 java.lang 包中,所以不需要 import)祖先类。
extends关键字
在 Java 中,类的继承是单一继承,也就是说,一个子类只能拥有一个父类,所以 extends 只能继承一个类。
public class Animal {
private String name;
private int id;
public Animal(String myName, String myid) {
//初始化属性值
}
public void eat() { //吃东西方法的具体实现 }
public void sleep() { //睡觉方法的具体实现 }
}
public class Penguin extends Animal{
}
接口:
在JAVA编程语言中是一个抽象类型,是抽象方法的集合,接口通常以interface来声明。一个类通过继承接口的方式,从而来继承接口的抽象方法。
接口并不是类,编写接口的方式和类很相似,但是它们属于不同的概念。类描述对象的属性和方法。接口则包含类要实现的方法。
除非实现接口的类是抽象类,否则该类要定义接口中的所有方法。
接口无法被实例化,但是可以被实现。一个实现接口的类,必须实现接口内所描述的所有方法,否则就必须声明为抽象类。另外,在 Java 中,接口类型可用来声明一个变量,他们可以成为一个空指针,或是被绑定在一个以此接口实现的对象。
implements关键字
使用 implements 关键字可以变相的使java具有多继承的特性(但是本质是接口),使用范围为类继承接口的情况,可以同时继承多个接口(接口跟接口之间采用逗号分隔)。
public interface A {
public void eat();
public void sleep();
}
public interface B {
public void show();
}
public class C implements A,B {
}
多态
多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。
多态的作用:把不同的子类对象都当作父类来看,可以屏蔽不同子类对象之间的差异,写出通用的代码,做出通用的编程,以适应需求的不断变化。让程序具有扩展性,节省成本,提高效率。
多态是同一个行为具有多个不同表现形式或形态的能力。
多态就是同一个接口,使用不同的实例而执行不同操作。
发生多态的三个前提条件:
继承。多态发生一定要子类和父类之间。
覆盖。子类覆盖了父类的方法。
声明的变量类型是父类类型,但实例则指向子类实例。
public class Wine {
public void fun1(){
System.out.println("Wine 的Fun.....");
fun2();
}
public void fun2(){
System.out.println("Wine 的Fun2...");
}
}
public class JNC extends Wine{
/**
* @desc 子类重载父类方法
* 父类中不存在该方法,向上转型后,父类是不能引用该方法的
* @param a
* @return void
*/
public void fun1(String a){
System.out.println("JNC 的 Fun1...");
fun2();
}
/**
* 子类重写父类方法
* 指向子类的父类引用调用fun2时,必定是调用该方法
*/
public void fun2(){
System.out.println("JNC 的Fun2...");
}
}
public class Test {
public static void main(String[] args) {
Wine a = new JNC(); //向上转型
a.fun1();
}
}
-------------------------------------------------
Output:
Wine 的Fun.....
JNC 的Fun2...
a.fun1()首先是运行父类Wine中的fun1().然后再运行子类JNC中的fun2()。
分析:在这个程序中子类JNC重载了父类Wine的方法fun1(),重写fun2(),而且重载后的fun1(String a)与 fun1()不是同一个方法,由于父类中没有该方法,向上转型后会丢失该方法,所以执行JNC的Wine类型引用是不能引用fun1(String a)方法。而子类JNC重写了fun2() ,那么指向JNC的Wine引用会调用JNC中fun2()方法。
指向子类的父类引用由于向上转型了,它只能访问父类中拥有的方法和属性,而对于子类中存在而父类中不存在的方法,该引用是不能使用的,尽管是重载该方法。若子类重写了父类中的某些方法,在调用该些方法的时候,必定是使用子类中定义的这些方法(动态连接、动态调用)。
Java实现多态有三个必要条件:继承、重写、向上转型。
继承:在多态中必须存在有继承关系的子类和父类。
重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。
Java中多态的实现方式:接口实现,继承父类进行方法重写,同一个类中进行方法重载。
1基于接口实现的多态
继承是通过重写父类的同一方法的几个不同子类来体现的,那么就可就是通过实现接口并覆盖接口中同一方法的几不同的类体现的。
在接口的多态中,指向接口的引用必须是指定这实现了该接口的一个类的实例程序,在运行时,根据对象引用的实际类型来执行对应的方法。
继承都是单继承,只能为一组相关的类提供一致的服务接口。但是接口可以是多继承多实现,它能够利用一组相关或者不相关的接口进行组合与扩充,能够对外提供一致的服务接口。所以它相对于继承来说有更好的灵活性。
2基于继承实现的多态
基于继承的实现机制主要表现在父类和继承该父类的一个或多个子类对某些方法的重写,多个子类对同一方法的重写可以表现出不同的行为。
JAVA 怎么实现多态?
方法的动态绑定, 通过方法表,方法表有继承自父类 的方法及各自新定义的方法。因此,方法表的偏移量总是固定的。所有继承父类的子类的方法表中,其父类所定义的方法的偏移量也总是一个定值。这样 JVM 在调用实例方法只需要通过Invokevirtual 指令调用方法表中的第几个方法 。如果是实现多个接口,就要遍历搜索方法表,所以接口方法慢于类方法的调用的。
C++ 怎么实现多态 ? 和java 区别是什么?
和Java差不多,虚函数 加 vitrual 关键字,编译器会为每个包含虚函数的类创建一个虚表 ,在这个数组中存放每个虚函数的地址,每个对象有一个指针,指向所属类的虚表,在程序运行时,根据对象的类型去初始化这个指针指向了所属类的虚表。
但是Java 的方法表中包含 Java 类所定义的所有实例方法,C++ 虚表只包含vitrual的方法 。Java 类方法都相当于C++的虚方法,任意 Java 对象都只有一个方法表,而 C++ 在多重继承下可能指向多个方法表
JAVA与C++区别:
- Java是解释型语言,所谓的解释型语言,就是源码会先经过一次编译,成为中间码,中间码再被解释器解释成机器码。对于Java而言,中间码就是字节码(.class),而解释器在JVM中内置了。
- C++是编译型语言,所谓编译型语言,就是源码一次编译,直接在编译的过程中链接了,形成了机器码。
- C++比Java执行速度快,但是Java可以利用JVM跨平台。
二、多重继承
Java不支持多重继承。多重继承,它允许多父类派生一个子类。也就是说,一个类允许继承多个父类。尽管多重继承功能很强,但使用复杂,而且会引起许多麻烦,编译程序实现它也很不容易。所以 Java 不支持多重继承,但允许一个类实现多个接口。可见,Java 既保留了 C++多重继承的功能,又避免了 C++的许多缺陷。
三、数据类型
Java 是完全面向对象的语言,所有方法和数据都必须是类的一部分。除了基本数据类型之外,其余类型的数据都作为对象型数据。例如,对象型数据包括字符串和数组。类将数据和方法结合起来,把它们封装在其中,这样每个对象都可实现具有自己特点的行为。而 C++将函数和变量定义为全局的,然后再来调用这些函数和变量,从而增加了程序的负担。此外,Java 还取消了 C/C++中的结构和联合,使编译程序更加简洁。
而C++中还有面向过程的东西,比如是全局变量和全局函数。
四、自动内存管理
Java 自动进行无用内存回收操作,不再需要程序员进行手动删除。Java 程序中所有的对象都是用 new 操作符建立在堆栈上的,这个操作符类似于 C++的“new”操作符。当 Java 中一个对象不再被用到时,无须使用内存回收器,只需要给它添加删除标签,无用内存的回收器便利用空闲时间在后台运行。而 C++中必须由程序释放内存资源,这就增加了程序员的负担。
C++中,开发需要自己去管理内存,但是Java中JVM有自己的GC机制,虽然有自己的GC机制,但是也会出现OOM和内存泄漏的问题。C++中有析构函数,Java中Object的finalize方法。
五、操作符重载
Java 不支持操作符重载,操作符重载被认为是 C++的突出特征。操作符重载,就是把操作符(比如’+,-,*,/'这些运算符)赋于新的意义, 来完成更为细致具体的运算等功能。要实现操作符重载,就要使用操作符重载函数,而运用函数就肯定会存在各种限制条件以及特殊情况。特殊情况就需特殊处理,因此操作符重载还是比较繁琐的。
六、预处理功能
C/C++在编译过程中都有一个预编译阶段,即预处理器。预处理器为开发人员提供了方便,但增加了编译的复杂性。Java 允许预处理,但不支持预处理器功能,因为 Java 没有预处理器,所以为了实现预处理,它提供了引入语句(import),但它与 C++预处理器的功能类似。
八、字符串
C 和 C++不支持字符串变量,在 C 和 C++程序中使用“Null”终止符代表字符串的结束。在 Java 中字符串是用类对象(String 和 StringBuffer)来实现的,在整个系统中建立字符串和访问字符串元素的方法是一致的。Java 字符串类是作为 Java 语言的一部分定义的,而不是作为外加的延伸部分。此外,Java 还可以对字符串用“+”进行连接操作。
十、类型转换
在 C 和 C++中,有时会出现数据类型的隐含转换,这就涉及了自动强制类型转换问题。例如,在 C++中可将一个浮点值赋予整型变量,并去掉其尾数。Java 不支持 C++中的自动强制类型转换,如果需要,必须由程序显式进行强制类型转换。
- 抽象和接口:
从设计层面来说,抽象是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。
含有abstract修饰符的class即为抽象类,abstract 类不能创建的实例对象。abstract class类中定义抽象方法必须在具体(Concrete)子类中实现,所以,不能有抽象构造方法或抽象静态方法。如果的子类没有实现抽象父类中的所有抽象方法,那么子类也必须定义为abstract类型。
interface 可以说成是抽象类的一种特例,接口中的所有方法都必须是抽象的。
接口中的方法定义默认为public abstract类型,接口中的成员变量类型默认为public static final。
相同点:
1.都可以被继承;
2.都不能实例化;
3.都可以包含方法声明;
4.派生类必须实现未实现的方法。
语法区别:
1.抽象类可以有构造方法,接口中不能有构造方法。
2.抽象类中可以有普通成员变量,接口中没有普通成员变量
3.抽象类中可以包含非抽象的普通方法,接口中的所有方法必须都是抽象的,不能有非抽象的普通方法。
4. 抽象类中的抽象方法的访问类型可以是public,protected和(默认类型,虽然eclipse下不报错,但应该也不行),但接口中的抽象方法只能是public类型的,并且默认即为public abstract类型。
5. 抽象类中可以包含静态方法,接口中不能包含静态方法
6. 抽象类和接口中都可以包含静态成员变量,抽象类中的静态成员变量的访问类型可以任意,但接口中定义的变量只能是public static final类型,并且默认即为public static final类型。
7. 一个类可以实现多个接口,但只能继承一个抽象类。
两者在应用上的区别:
接口更多的是在系统架构设计方法发挥作用,主要用于定义模块之间的通信契约。
而抽象类在代码实现方面发挥作用,可以实现代码的重用.
java中为什么要单继承,多实现?
单继承:
若为多继承,那么当多个父类中有重复的属性或者方法时,子类的调用结果会含糊不清,因此用了单继承。
多实现:
但是,通过实现接口拓展了类的功能,若实现的多个接口中有重复的方法也没关系,因为实现类中必须重写接口中的方法,所以调用时还是调用的实现类中重写的方法。
接口中重复的变量:所有属性都是 static final修饰的,即常量,在编译时期确定了其值。若在使用时,两个相同的常量值不同,在编译时期就不能通过。
继承类执行顺序
- 父类–静态变量
- 父类–静态初始化块
- 子类–静态变量
- 子类–静态初始化块
子类主程序main
父类的非静态代码块(变量,初始化块)
父类的有参构造函数–父亲的名字
父类的方法
父类的非静态代码块
父类的无参构造函数
子类的非静态代码块
子类的有参构造函数–儿子的名字
子类Override了父类的方法
方法重载和方法重写(覆盖)的区别?
① 方法重载是同一个类中具有不同参数列表的同名方法(无关返回值类型),
方法重写是子类中具有和父类相同参数列表的同名方法,会覆盖父类原有的方法。
②重载的返回值类型和权限修饰符,异常抛出类型没有要求,
重写方法的返回值类型小于等于父类被重写方法的返回值类型,修饰符权限大于等于父类被重写方法权限修饰符,抛出的异常类型小于等于父类被重写方法抛出的异常类型。
泛型
泛型 = “参数化类型”。就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
编译器会在编译期间擦除泛型语法并相应的做出一些类型转换动作,以 Object 或者extends xxx 的xxx 来替代 擦除是为了兼容老程序,需为原本不支持泛型的 API 平行添加一套泛型 API(主要是容器类型)。
C++ 泛型的不同之处:
编译器从函数模板通过具体类型产生不同的函数;编译器会对函数模板进行两次编译:在声明的地方对模板代码本身进行编译,在调用的地方对参数替换后的代码进行编译。 是真正的范型**
泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?
顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),
然后在使用/调用时传入具体的类型(类型实参)。
泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,
操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
泛型只在编译阶段有效。
List<String> stringArrayList = new ArrayList<String>();
List<Integer> integerArrayList = new ArrayList<Integer>();
Class classStringArrayList = stringArrayList.getClass();
Class classIntegerArrayList = integerArrayList.getClass();
if(classStringArrayList.equals(classIntegerArrayList)){
Log.d("泛型测试","类型相同");
}
结果是,会输出!!!
在编译之后程序会采取去泛型化的措施。也就是说Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。
对此总结成一句话:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。
泛型的使用
泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法
泛型类
泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。
//传入的实参类型需与泛型的类型参数类型相同,即为Integer.
Generic<Integer> genericInteger = new Generic<Integer>(123456);
泛型接口
泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中
public interface Generator<T>//定义一个泛型接口
public class FruitGenerator implements Generator<String>
泛型通配符
我们知道Ingeter是Number的一个子类,同时在特性章节中我们也验证过Generic与Generic实际上是相同的一种基本类型。
但是,在使用Generic作为形参的方法中,不能使用Generic的实例传入;在逻辑上类似于Generic和Generic不能看成具有父子关系的泛型类型。Generic不能被看作为`Generic的子类。由此可以看出:同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。
需要一个在逻辑上可以表示同时是Generic和Generic父类的引用类型。由此类型通配符应运而生。
public void showKeyValue1(Generic<?> obj)
类型通配符一般是使用?代替具体的类型实参,注意了,此处’?’是类型实参,而不是类型形参 。重要说三遍!此处’?’是类型实参,而不是类型形参 ! 此处’?’是类型实参,而不是类型形参 !再直白点的意思就是,此处的?和Number、String、Integer一样都是一种实际的类型,可以把?看成所有类型的父类。是一种真实的类型。
可以解决当具体类型不确定的时候,这个通配符就是 ? ;当操作类型时,不需要使用类型的具体功能时,只使用Object类中的功能。那么可以用 ? 通配符来表未知类型。
泛型方法
在java中,泛型类的定义非常简单,但是泛型方法就比较复杂了。
public static <T> T getMiddle(T... a) {
return a[a.length / 2];
}
这个方法是在普通类中定义的,而不是在泛型类中定义的。然而,这是一个泛型方法,可以从尖括号和类型变量看出这一点。注意,类型变量放在修饰符(这里是 public static)的后面,返回类型的前面。
static和final
final 关键字主要用在三个地方:
修饰变量
final成员变量表示常量,一但被赋值,值不可以再被改变。
当final修饰一个基本数据类型时,该基本数据类型的值在初始化之后便不能再改变。
当final修饰一个引用类型时,在初始化之后不能再让其指向其他对象,但是对象的内容是可以改变的。
修饰类 用final修饰类,表示这个类不能被继承。
此时类中的所有成员方法,都会被隐式地指定为 final 方法。
修饰方法 即父类的final方法是不可以被子类继承重写的。
目的是把方法锁定,防止任何继承类修改它的含义。
static 关键字主要有以下四种使用场景:
对于静态变量在内存中只有一个拷贝(节省内存),JVM只为静态分配一次内存,在加载类的过程中完成静态变量的内存分配。
static修饰的属性的初始化在编译期(类加载的时候),初始化后能改变。
static修饰的属性所有对象都只有一个值。
static修饰的属性强调它们只有一个。修饰的变量是全局的 所以对象共享。
(1)修饰成员变量和成员方法: 被 static 修饰的成员属于类,不属于这个类的某个对象,被类中所有的对象共享,可以并且建议通过类名进行调用。被 static 声明的成员变量属于静态成员变量,静态变量存放在 Java 内存区域的方法区,调用格式:类名.静态变量名、类名.静态方法名();
(2)静态代码块: 静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—>非静态代码块—>构造方法),当前类不管创建多少对象,静态代码块都只执行一次;
(3)静态内部类(static 修饰类的话只能修饰内部类): 静态内部类与非静态内部类之间存在一个最大的区别: 非静态内部类在编译完成之后,会隐含地保存着一个引用,该引用指向创建它的外围类;但是静态内部类却没有,这意味着:
- 它的创建不需要依赖外围类的创建;
- 它不能使用任何外围类的非static成员变量和方法;
举例:
public class OuterClass {
public static class InnerClass {
InnerClass(){
System.out.println("===== 我是一个内部类'InnerClass' =====");
}
}
}
public class TestStaticClass {
public static void main(String[] args) {
// 不需要先new一个InnerClass的对象,直接使用
OuterClass.InnerClass inner = new OuterClass.InnerClass();
}
}
要是没加static的话,这里要:
// 需要先new一个OuterClass的对象outer
// 然后通过outer.new生成内部类的对象
OuterClass outer = new OuterClass();
OuterClass.InnerClass inner = outer.new InnerClass();
(4)静态导包(用来导入类中的静态资源): 格式为:import static xxx,可以导入某个类中的指定静态资源,并且不需要显式使用类名来调用类中的静态成员和静态方法。
static修饰的成员只实例化一次,而string类型每次赋值都会重新创建一个实例,那么用static修饰string呢?
string 类型每次实例化都会重新创建一个实例:
正确:string 类型重载了运算符 “=” ,每次 “=” 操作都是一次 “new”。
static修饰的成员只实例化一次:
错误:static 成员独立于对象之外,没有说只能实例化一次。
用static修饰类只能修饰内部类。普通类是不允许声明为静态的,只有内部类才可以。
被static修饰的内部类可以直接作为一个普通类来使用,而不需先实例一个外部类。
所以static是能修饰所有内部类的!
final、finally、finalize的区别与用法
- final用于声明属性,方法和类,分别表示属性不可交变,方法不可覆盖,类不可继承。
- finally是java的一种异常处理机制。异常处理语句结构的一部分,表示总是执行。
- finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法,供垃圾收集时的其他资源回收,例如关闭文件等。finalize()方法在垃圾收集器将对象从内存中清除出去前,做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没被引用时对这个对象调用的。它是在Object类中定义的,因此所的类都继承了它。子类覆盖finalize()方法以整理系统资源或者执行其他清理工作。finalize()方法是在垃圾收集器删除对象之前对这个对象调用的。
八大基本数据类型:
long/double–>int/float–>short/char–>byte–>boolean
对应着64位bit–>32位–>16位–>8位–>不确定
包装类:
INTEGER等等。
包装类有三个用法 一个实现基本类型之间的转换 二是便于函数传值 三就是在一些地方要用到Object的时候方便将基本数据类型装换
基本数据类型和包装类型有什么区别?
1、包装类是对象,拥有方法和字段,对象的调用都是通过引用对象的地址,基本类型不是
2、包装类型是引用的传递,基本类型是值的传递
3、声明方式不同,
基本数据类型不需要new关键字,而包装类型需要new在堆内存中进行new来分配内存空间
4、存储位置不同,
基本数据类型直接将变量值保存在栈中,而包装类型是把对象放在堆中,然后通过对象的引用来调用他们
5、初始值不同,
基本类型的初始值如int为0,boolean为false,而包装类型的初始值为null
6、使用方式不同,
基本类型直接赋值直接使用就好,而包装类型在集合如Collection、Map时会使用到。
int和Integer的区别
int类型:新建后存储:
基本数据类型存放位置:
方法参数、局部变量存放在栈内存中的栈桢中的局部变量表
常量存放在常量池中
Integer类型:新建后存储:
默认-128—127存储在方法区(常量池),之外数据存储在堆中
1、Integer是int的包装类,int则是java的一种基本数据类型
2、Integer变量必须实例化后才能使用,而int变量不需要
3、Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int则是直接存储数据值
4、Integer的默认值是null,int的默认值是0
关于Integer和int的比较
1、由于Integer变量实际上是对一个Integer对象的引用,所以两个通过new生成的Integer变量永远是不相等的(因为new生成的是两个对象,其内存地址不同)。
Integer i = new Integer(100);
Integer j = new Integer(100);
System.out.print(i == j); //false
2、Integer变量和int变量比较时,只要两个变量的值是向等的,则结果为true(因为包装类Integer和基本数据类型int比较时,java会自动拆包装为int,然后进行比较,实际上就变为两个int变量的比较)
Integer i = new Integer(100);
int j = 100;
System.out.print(i == j); //true
3、非new生成的Integer变量和new Integer()生成的变量比较时,结果为false。(因为非new生成的Integer变量指向的是java常量池中的对象,而new Integer()生成的变量指向堆中新建的对象,两者在内存中的地址不同)
Integer i = new Integer(100);
Integer j = 100;
System.out.print(i == j); //false
4、对于两个非new生成的Integer对象,进行比较时,如果两个变量的值在区间-128到127之间,则比较结果为true,如果两个变量的值不在此区间,则比较结果为false
Integer i = 100;
Integer j = 100;
System.out.print(i == j); //true
Integer i = 128;
Integer j = 128;
System.out.print(i == j); //false
String,StringBuffer与StringBuilder的区别
- String是一个不可变类,指的是它在被定义后不能被修改,有些人会说明明可以修改啊,此处的不能修改指的是一个string被定义后改变不了该内存存储的值,
当你在进行第二次赋值操作的时候数据库并不会修改这个内存地址的对应的值,而是重新指向一个地址储存,所以每改变一次就会多占有个位置。都会放在字符串常量池中。
每进行一次赋值“=”,就会创建一个新对象。
String str ="abc";
str = str +"def";//这一步jvm会再次创建一个String对象
第二次其实jvm又生成了一个String类,而不是直接覆盖原来的"abc",因此我们说String类是不可改变类。这一种特性会带来一个问题,每次拼接都要创建都要创建一次对象,当我们要拼接大量字符串的时候,效率会变得非常非常慢。实际上这里一共创建了3个对象!
2. StringBuffer是一个可变的类,但是它是一个线程安全的类。故在执行速度上有所欠缺,但在有高并发情况出现的时候还是比较常的使用它。
StringBuffer sb =new StringBuffer("abc");
sb.append("efg");//并没有创建一个新的对象
这里第二步并没有产生一个新的对象,而是在原来的基础上追加字符串,这种方式在拼接字符串的时候效率肯定比String要高得多。线程安全,用synchronized修饰的。
3. StringBuilder 他们的原理和操作基本相同,区别在于StringBuffer支持并发操作,线性安全的,适合多线程中使用。StringBuilder不支持并发操作,线性不安全的,不适合多线程中使用。新引入的StringBuilder类不是线程安全的,但其在单线程中的性能比StringBuffer高。
小结:(1)如果要操作少量的数据用 String;
(2)单线程操作字符串缓冲区下操作大量数据 StringBuilder。
(3)多线程操作字符串缓冲区下操作大量数据 StringBuffer;
为什么String类是不可变的?
string的jdk源码
1.看到String类被final修饰。这里你就要说出被final修饰的类不能被继承,方法不能被重写,变量不能被修改。
2.看到final修饰的char[]代表了被存储的数据不可更改性。但是:虽然final代表了不可变,但仅仅是引用地址不可变,并不代表了数组本身不会变,值还是可以改变的。所以在这里只有final修饰是不能确保string的不可变性。还需要privite修饰。才能确保不可变性。
原因:
因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。
因为字符串是不可变的,所以在它创建的时候HashCode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。
String为什么设计成final的?
这说明String不可继承。再看下面,String类的主力成员字段value是个char[ ]数组,而且是用final修饰的。final修饰的字段创建以后就不可改变。
这个最简单的原因,就是为了安全。
还有一个大家都知道,就是在并发场景下,多个线程同时读一个资源,是不会引发竟态条件的。只有对资源做写操作才有危险。不可变对象不能被写,所以线程安全。
自定义String类为什么不会被加载?
当自己定义了一个String类,且包名也是java.lang,为什么不会被加载,
因为双亲委派机制,
当加载String类时,会一层一层的往上委托,直到在启动类加载器Bootstrap ClassLoader,而启动类加载器能够加载String类,因为在java包下有,所以加载的是Java中的String,而不是自定义的。
判断字符串是否为空的方法
先说明:null和“”的区别
null不是对象,"“是对象,所以null没有分配空间,”"分配了空间,例如:
String str1 = null; str引用为空
String str2 = “”; str引用一个空串
str1还不是一个实例化的对象,而str2已经实例化。
对象用equals比较,null用等号比较。
方法一:直观, 方便, 但效率很低:
if(s == null ||"".equals(s));
方法二: 比较字符串长度, 效率高, 是我知道的最好一个方法:
if(s == null || s.length() <= 0);
方法三: Java SE 6.0 才开始提供的方法, 效率和方法二几乎相等, 但出于兼容性考虑, 推荐使用方法二.
if(s == null || s.isEmpty());
方法四: 这是一种比较直观,简便的方法,而且效率也非常的高,与方法二、三的效率差不多:
if (s == null || s == “”);
注意:s == null 是有必要存在的.
如果 String 类型为null, 而去进行 equals(String) 或 length() 等操作会抛出java.lang.NullPointerException.
并且s==null 的顺序必须出现在前面,不然同样会抛出java.lang.NullPointerException.
其他方法:
使用StringUtils的API:
1)isEmpty:
StringUtils.isEmpty( null ) = true
StringUtils.isEmpty( “” ) = true
StringUtils.isEmpty( " " ) = false //注意在 StringUtils 中空格作非空处理
2)isBlank:
StringUtils.isBlank( null ) = true
StringUtils.isBlank( “” ) = true
StringUtils.isBlank( " " ) = true
StringUtils.isBlank( “\t \n \f \r” ) = true //对于制表符、换行符、换页符和回车符
isEmpty底层源码和方法二一样:
public static boolean isEmpty(CharSequence cs){
return (cs == null) || (cs.length() == 0);
}
因此效率差不多。
isBlank底层源码复杂一些,在此之后还要判断空格。效率低一些。
equals,hashcode和==的区别
==
1.基本数据类型,也称原始数据类型
byte,short,char,int,long,float,double,boolean之间,双等号比较的是他们的值。
2.引用类型(类、接口、数组)
用双等号进行比较的时候,比较的是他们在内存中的存放地址,即两个引用指向的是否是同一个对象。所以,除非是同一个new出来的对象,他们的比较后的结果为true,否则比较后结果为false。
对象是放在堆中的,栈中存放的是对象的引用(地址)。由此可见是对栈中的值进行比较的。如果要比较堆中对象的内容是否相同,那么就要重写equals方法了。
equals
1、默认情况(没有覆盖equals方法)下equals方法都是调用Object类的equals方法,而Object的equals方法主要用于判断对象的内存地址引用是不是同一个地址(是不是同一个对象)。定义的equals与==是等效的。
2 、要是类中覆盖了equals方法,那么就要根据具体的代码来确定equals方法的作用了,覆盖后一般都是通过对象的内容是否相等来判断对象是否相等。比如下面的String。
hashCode
hashCode()方法返回的就是一个数值,从方法的名称上就可以看出,其目的是生成一个hash码。hash码的主要用途就是在对对象进行散列的时候作为key输入,据此很容易推断出,我们需要每个对象的hash码尽可能不同,这样才能保证散列的存取性能。事实上,Object类提供的默认实现确实保证每个对象的hash码不同(在对象的内存地址基础上经过特定算法返回一个hash码)。
string的==、和equals:
先说equals():
两个普通对象所使用的equals()是超类(Object类)中的方法,Object类中的 equals() 方法判断的还是两个引用指向的是否为同一对象;
String 对象使用的是它自己的equals()方法,在String类的底层重写了超类(Object类)的equals() 和 hashCode() 方法,equals()先判断两个引用指向的是否为同一对象,如不为同一对象,再判断两个String对象的值是否一样。
String类重写equals()的源码:
public boolean equals(Object anObject) {
//引用不同,不相等
if (this == anObject) {
return true;
}
//类型不同,不相等
if (!(anObject instanceof String)) {
return false;
}
String anotherString = (String) anObject;
int n = value.length;
//长度不等,不相等
if (n != anotherString.value.length) {
return false;
}
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
//字符不对等,不相等
if (v1[i] != v2[i])
return false;
i++;
}
//排除所有不符合的情况,即相等
return true;
}
再说== (双等号)
对于基本数据类型,== 号比较的是值;
对于引用类型, == 号比较的是两个对象的地址值是否一样,即两个引用指向的是否是同一个对象。String就是这样。
String str1 = “a” + “b” + “c” + 1 + 2 + 3; 由于"a" + “b” + “c” + 1 + 2 + 3 都是常量值,所以在编译阶段会被编译成str1 = “abc123”; 如果在常量池中没有该字符串,JVM会将其字符串放入常量池;
再到 str2 = “abc123” 时,由于常量池中已有该字符串,所以不会再生成新的字符串,str2 指向的就是常量池中的那个 “abc123”, 所以 str1 == str2 返回 true;
str3 使用 new 来创建字符串对象,JVM 会在堆中开辟一块内存来存放该字符串对象,所以 str3 存放的是堆中的该对象地址,而不是常量池中的(abc123)字符串。str2 指向的是常量池中的字符串,str3 指向的是堆中的对象,所以str2 == str3 返回 false。
Object类的方法:
1.getClass方法
获取运行时类型,返回值为Class对象
2.hashCode方法
返回该对象的哈希码值,是为了提高哈希表的性能(HashTable)
3.equals方法
判断两个对象是否相等,在Object源码中equals就是使用==‘’去判断,所以在Object中equals是等价于==的,但是在String及某些类对equals进行了重写,实现不同的比较。
5.toString方法
返回一个String字符串,用于描述当前对象的信息,可以重写返回对自己有用的信息,默认返回的是当前对象的类名+hashCode的16进制数字。
6.wait方法
多线程时用到的方法,作用是让当前线程进入等待状态,同时也会让当前线程释放它所持有的锁。直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,当前线程被唤醒
7.notify方法
多线程时用到的方法,唤醒该对象等待的某个线程
8.notifyAll方法
多线程时用到的方法,唤醒该对象等待的所有线程
9.finalize
对象在被GC释放之前一定会调用finalize方法,对象被释放前最后的挣扎,因为无法确定该方法什么时候被调用,很少使用。
异常Throwable分类:
Java语言按照错误严重性,从throwale根类衍生出Error和Exception两大派系。
Throwable 是 Java 语言中所有错误或异常的超类,在 Java 中只有 Throwable 类型的实例才可以被抛出(throw)或者捕获(catch),它是异常处理机制的基本组成类型。
Error(错误):程序在执行过程中所遇到的硬件或操作系统的错误。错误对程序而言是致命的,将导致程序无法运行。
指 java 运行时系统的内部错误和资源耗尽错误。应用程序不会抛出该类对象。如果出现了这样的错误,除了告知用户,剩下的就是尽力使程序安全的终止。
常见的错误有内存溢出,jvm虚拟机自身的非正常运行,class文件没有主方法。程序本生是不能处理错误的,只能依靠外界干预。Error是系统内部的错误,由jvm抛出,交给系统来处理。
EXCEPTION(异常):是程序正常运行中,可以预料的意外情况。
是程序本身可以处理的异常。
比如数据库连接中断,空指针,数组下标越界。异常出现可以导致程序非正常终止,也可以预先检测,被捕获处理掉,使程序继续运行。
一个是运行时异常 RuntimeException , 一 个是可检查异常 CheckedException。
- 编译时异常CheckedException:
又叫可检查异常,通常时由语法错和环境因素(外部资源)造成的异常。比如输入输出异常IOException,数据库操作SQLException。其特点是,Java语言强制要求捕获和处理所有非运行时异常。通过行为规范,强化程序的健壮性和安全性。
受检查的异常,这种异常是强制我们catch或throw的异常。你遇到这种异常必须进行catch或throw,如果不处理,编译器会报错。比如:IOException。
运行时异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过。 - 运行时异常RuntimeException:
又叫不检查异常,这些异常一般是由程序逻辑错误引起的,即语义错。
从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。
比如算术异常,空指针异常NullPointerException,下标越界IndexOutOfBoundsException。运行时异常应该在程序测试期间被暴露出来,由程序员去调试,而避免捕获。
RuntimeException常见异常:
1 NullPointerException,空指针异常。
2 NumberFormatException,字符串转化成数字时。
3 ArrayIndexOutOfBoundsException, 数组越界时。
4 StringIndexOutOfBoundsException, 字符串越界时。
5 ClassCastException,类型转换时。
6 UnsupportedOperationException,该操作不支持,一般子类不实现父类的某些方法时。
7 ArithmeticException,零作为除数等。
8 IllegalArgumentException,表明传递了一个不合法或不正确的参数
运行时出现错误,说明你的代码有问题,程序已经无法继续运行,所以对RuntimeException的处理时不必要的。之所以不处理,目的就是要是程序停止,修正代码。
try catch finally异常机制JVM字节码
案例一:
这里有个很有趣的地方,明明代码里面只有一个finally,为什么这里有三个finally???那就是一个面试必问点,finally是异常机制中必定执行的语句块,在JVM字节码指令中为了确保它一定被执行,所有地方都会加一段这个finally要执行的代码。
(1)第一个地方,假设没有异常,正常执行后也执行finally里面的输出
(2)第二个地方。有异常,捕获处理完后,也执行finally里面的输出
(3)第三个地方。有异常并且没有捕获到,因为我们定义的运行时异常很多,如果指定的Type类型不对应catch到的,或者是Throwable父类一些特殊异常,那就没办法正确处理,所以这一个finally都是用来捕获漏网之鱼的。
所以!finally在字节码中有三份!
序列化(JavaBean常用操作):
把Java对象转换为字节序列(json)的过程。
反序列化:把字节序列恢复为Java对象的过程。
对象的序列化主要有两种用途:
1) 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
2) 在网络上传送对象的字节序列。以达到以后恢复成原来的对象。
使用场景:所有可在网络上传输的对象都必须是可序列化的,比如RMI(remote method invoke,即远程方法调用),传入的参数或返回的对象都是可序列化的,否则会出错;所有需要保存到磁盘的java对象都必须是可序列化的。
通常建议:程序创建的每个JavaBean类都实现Serializeable接口。
序列化的目的有两个,第一个是便于存储,第二个是便于传输。
常用:
阿里巴巴提供的fastjson,当用json转换实体类时:
–无get开头的方法,将找不到序列器。
–如果有get开头的方法,但是无此get方法后面的字段,也找不到序列器[元数据一体化的项目落到此坑]。
–证明它与get开头的方法有关。
–fastJson在转换java对象为json的时候,fastjson默认转换是不序列化null值对应的key的。
java开源的Jackson类,也与get开头的方法有关。
Google提供的Gson,该gson序列化只与属性(字段)有关,与get开头的方法无关。
java获取类信息
我们写的代码是存储在后缀名是 .java的文件里的,但是它会被编译,最终真正去执行的是编译后的 .class文件。Java是面向对象的语言,一切皆对象,所以java认为 这些编译后的 class文件,这种事物也是一种对象,它也给抽象成了一种类,这个类就是Class。
想获取一个类的运行时类型信息并加以使用时,有三种方法:
1通过一个实例对象获取一个类的Class对象,其中的getClass()是从顶级类Object继承而来的,它将返回表示该对象的实际类型的Class对象引用。
Gum gum = new Gum();
Class clazz2=gum.getClass();
2可以调用Class.forName()方法获取Class对象的引用,这样做的好处是无需通过持有该类的实例对象引用而去获取Class对象。
Class clazz=Class.forName(“com.zejian.Gum”);
3字面常量的方式”.class”
Class initable = Initable.class;
其中实例类的getClass方法和Class类的静态方法forName都将会触发类的初始化阶段,而字面常量获取Class对象的方式则不会触发初始化。
初始化是类加载的最后一个阶段,也就是说完成这个阶段后类也就加载到内存中(Class对象在加载阶段已被创建),此时可以对类进行各种必要的操作了(如new对象,调用静态成员等),注意在这个阶段,才真正开始执行类中定义的Java程序代码或者字节码。
关于类加载的初始化阶段,在虚拟机规范严格规定了有且只有5种场景必须对类进行初始化:
1使用new关键字实例化对象时、读取或者设置一个类的静态字段(不包含编译期常量)以及调用静态方法的时候,必须触发类加载的初始化过程(类加载过程最终阶段)。
2使用反射包(java.lang.reflect)的方法对类进行反射调用时,如果类还没有被初始化,则需先进行初始化,这点对反射很重要。
3当初始化一个类的时候,如果其父类还没进行初始化则需先触发其父类的初始化。
4当Java虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的类),虚拟机会先初始化这个主类。
5当使用JDK 1.7 的动态语言支持时,如果一个java.lang.invoke.MethodHandle 实例最后解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应类没有初始化时,必须触发其初始化。
反射:
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法;这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制。
缺点是破坏了封装性及泛型约束。
把Java类中的各个成分映射成一个个的Java对象。
反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法。
Java让我们在运行时识别对象和类的信息,主要有2种方式:
一种是传统的RTTI,它假定我们在编译时已经知道了所有的类型信息;
另一种是反射机制,它允许我们在运行时发现和使用类的信息。
流程:JVM只是简单地检查这个对象,看它属于哪个特定的类。因此,那个类的.class对于JVM来说必须是可获取的,要么在本地机器上,要么从网络获取。
对于RTTI和反射之间的真正区别只在于:
RTTI,编译器在编译时打开和检查.class文件
反射,运行时打开和检查.class文件
在反射中,要获取一个类或调用一个类的方法,我们首先需要获取到该类的 Class 对象。
在 Java API 中,获取 Class 类对象有三种方法:
第一种,使用 Class.forName 静态方法。当你知道该类的全路径名时,你可以使用该方法获取 Class 类对象。
第二种,使用 .class 方法。这种方法只适合在编译前就知道操作的 Class。
第三种,使用类对象的 getClass() 方法。
通过反射创建类对象主要有两种方式:
第一种:通过 Class 对象的 newInstance() 方法。
Class clz = Apple.class;
Apple apple = (Apple)clz.newInstance();
第二种:通过 Constructor 对象的 newInstance() 方法
Class clz = Apple.class;
Constructor constructor = clz.getConstructor();
Apple apple = (Apple)constructor.newInstance();
反射的应用很多,很多框架都有用到:
spring 的 ioc/di 也是反射…
javaBean和jsp之间调用也是反射…
struts的 FormBean 和页面之间…也是通过反射调用…
JDBC 的 classForName()也是反射…
hibernate的 find(Class clazz) 也是反射…
反射性能损耗:
每一个Method都有一个root,不暴漏给外部,而是每次copy一个Method。具体的反射调用逻辑是委托给MethodAccessor的,而accessor对象会在第一次invoke的时候才创建,是一种lazy init方式。而且默认Class类会cache method对象。目前MethodAccessor的实现有两种,通过设置inflation,一个native方式,一种生成java bytecode方式。native方式启动快,但运行时间长了不如java方式,个人感觉应该是java方式运行长了,jit compiler可以进行优化。所以JDK6的实现,在native方式中,有一个计数器,当调用次数达到阀值,就会转为使用java方式。默认值是15。java方式的实现,基本和非反射方式相同。主要影响性能的问题,1是method.invoke中每次都要进行参数数组包装,2.在method.invoke中要进行方法可见性检查,3在accessor的java实现方式下,invoke时会检查参数的类型匹配。而在JDK7中methodhandle来做反射调用,形参和实参是准确的,所以只需要在链接方法的时候做检查,调用时不用再做检查。并且methodhandle是不可变值,所以jvm可以做激进优化,例如内联。
java反射是要解析字节码,将内存中的对象进行解析,包括了一些动态类型,所以抄JVM无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多!
动态代理:
参见AOP部分。
动态代理主要有两种实现,分别是JDK动态代理和cglib代理。
JDK动态代理,通过反射来接收被代理类,目标类必须实现某个接口,否则不可用;
CGLIB动态代理:当被代理类没有实现一个接口的时候,就会使用CGLIB进行动态代理。CGLIB动态代理通过运行时动态生成被代理类的子类,运用继承的方式来实现动态代理。如果被代理类被final修饰了,那么就不能使用CGLIB进行动态代理了。CGLIB底层是通过使用一个小而块的字节码处理框架ASM来转换字节码并生成新的类,覆盖或添加类中的方法。
从上述描述中不难看出,cglib类中方法类型不能设置为final。在执行效率上,早期的JDK动态代理效率远低于cglib,而随着JDK版本的更新,现在JDK动态代理的效率已经和cglib不相伯仲。
java创建对象的方式:
使用new关键字
ObjectName obj = new ObjectName();
使用反射机制
使用Class类的newInstance()方法:
java.lang.reflect.Constructor类里也有一个newInstance()方法(需要import这个包):
//方式一:
ObjectName obj = ObjectName.class.newInstance();
//方式二:
Class classA = Class.forName(“ClassName”);
ObjectName obj = (ObjectName) classA.newInstance();
//
ObjectName obj = ObjectName.class.getConstructor.newInstance();
使用clone方法
类必须先实现Cloneable接口并重写其clone()方法,才可使用该方法。
ObjectName obj = obj.clone();
使用反序列化
使用反序列化ObjectInputStream的readObject()方法:类必须实现Serializable接口
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_NAME))) {
ObjectName obj = ois.readObject();
}
浅拷贝和深拷贝
浅拷贝(shallowCopy)
只是增加了一个指针指向已存在的内存地址,
深拷贝(deepCopy)
是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存。
深拷贝和浅拷贝最根本的区别在于是否真正获取一个对象的复制实体,而不是引用。
Java中的浅拷贝与深拷贝:
Java中的对象拷贝(Object Copy)指的是将一个对象的所有属性(成员变量)拷贝到另一个有着相同类类型的对象中去。举例说明:比如,对象A和对象B都属于类S,具有属性a和b。那么对对象A进行拷贝操作赋值给对象B就是:B.a=A.a; B.b=A.b;
Java中的数据类型分为基本数据类型和引用数据类型。对于这两种数据类型,在进行赋值操作、用作方法参数或返回值时,会有值传递和引用(地址)传递的差别。
浅拷贝(Shallow Copy):
①对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。因为是两份不同的数据,所以对其中一个对象的该成员变量值进行修改,不会影响另一个对象拷贝得到的数据。
②对于数据类型是引用数据类型的成员变量,比如说成员变量是某个数组、某个类的对象等,那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值。
基本数据类型是值传递,所以修改值后不会影响另一个对象的该属性值;
引用数据类型是地址传递(引用传递),所以修改值后另一个对象的该属性值会同步被修改。
String类型非常特殊,所以我额外设置了一个字符串类型的成员变量来进行说明。首先,String类型属于引用数据类型,不属于基本数据类型,但是String类型的数据是存放在常量池中的,也就是无法修改的!也就是说,当我将name属性从“摇头耶稣”改为“大傻子"后,并不是修改了这个数据的值,而是把这个数据的引用从指向”摇头耶稣“这个常量改为了指向”大傻子“这个常量。在这种情况下,另一个对象的name属性值仍然指向”摇头耶稣“不会受到影响。
浅拷贝的实现方式主要有两种:
一、通过拷贝构造方法实现浅拷贝(拷贝构造方法指的是该类的构造方法参数为该类的对象)
二、通过重写clone()方法进行浅拷贝
深拷贝:
首先介绍对象图的概念。设想一下,一个类有一个对象,其成员变量中又有一个对象,该对象指向另一个对象,另一个对象又指向另一个对象,直到一个确定的实例。这就形成了对象图。那么,对于深拷贝来说,不仅要复制对象的所有基本数据类型的成员变量值,还要为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。也就是说,对象进行深拷贝要对整个对象图进行拷贝!
简单地说,深拷贝对引用数据类型的成员变量的对象图中所有的对象都开辟了内存空间;而浅拷贝只是传递地址指向,新的对象并没有对引用数据类型创建内存空间。
深拷贝的实现方法主要有两种:
一、通过重写clone方法来实现深拷贝
二、通过对象序列化实现深拷贝
总结:
基本数据类型:浅深拷贝都一样,都是复制变量值,新建变量后赋值进去;
引用数据类型:浅拷贝直接指针指向原来的地址(引用传递),深拷贝新开辟存储空间来存储原来的对象。