首页 / JAVA / Java三大特征详解
Java三大特征详解
内容导读
互联网集市收集整理的这篇技术教程文章主要介绍了Java三大特征详解,小编现在分享给大家,供广大互联网技能从业者学习和参考。文章包含13499字,纯文字阅读大概需要20分钟。
内容图文
![Java三大特征详解](/upload/InfoBanner/zyjiaocheng/605/2c4ed5a35dc24fa8a95d75707c6fa3bd.jpg)
转载请注明文章出处:https://blog.csdn.net/zengsao/article/details/112848632
文章目录
前言
对于熟悉Java语言的小伙伴来说,Java的三大特征都是我们耳熟能详的,其分别是封装,继承和多态,我们在开发的过程中经常就用到了这三大特征,但大家是否认真思考过这三大特征呢,封装的必要性是什么?继承能够给我们带来什么便利,多态的底层又是如何实现的,本篇博文将带大家详解了解!!!
一、封装
1、封装的概念
??封装性是面向对象编程的核心思想,指的就是将描述某种实体的数据和基于这些数的操作集合到一起,隐藏于类的内部,形成一个封装体。外部程序不被允许直接访问。只能通过该类提供的特定的方法来实现对隐藏信息的操作和访问,也就是说:不仅
要隐藏对象的信息
同时也要留出访问的接口
2、封装的好处
??只能通过规定方法访问数据
??隐藏类的实现细节
??方便修改实现
??方便加入控制语句
3、封装的实现
??字段:使用 private 修饰符修饰
??方法:也是一种封装,封装的实现的细节
??类: 也是一种封装,封装了多个方法和其他信息
## 4、封装的实例
public class test02 {
public static void main(String[] args) {
Person person = new Person();
person.age = 18;//可以直接访问
person.name = "zhangsan";//可以直接访问
person.setIdcard("123456789123456789");//通过相应接口,即setter方法才能访问
System.out.println(person.age+" "+person.getAge());
System.out.println(person.name+" "+person.getName());
System.out.println(person.getIdcard());
}
}
class Person{
public String name;
public int age;
/*private修饰符修饰的字段,外部不能直接访问
即指向该实体的引用不能通过xxx.idcard的方式访问该字段*/
private String idcard;
public String getName() {
return name;
}
public int getAge() {
return age;
}
public String getIdcard() {
return idcard;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public void setIdcard(String idcard) {
//通过在setter方法中添加条件控制语句来校验赋值的合法性
if(idcard.toCharArray().length == 18){//这里的校验只是为了说明
this.idcard = idcard;
}else{
System.out.println("身份证格式错误");
}
}
}
二、继承
1、继承的概念
??继承是面向对象最显著的一个特性。继承是从已有的类中派生出新的类,新的类能吸收已有类的数据属性和行为,并能扩展新的能力。Java语言中的类只支持单继承,如一个类只能有一个父类,而接口支持多继承。Java中多继承的功能是通过接口(interface)来间接实现的。继承关系是传递的。若类C继承类B,类B继承类A(多层继承),则类C既有从类B那里继承下来的属性与方法,也有从类A那里继承下来的属性与方法,还可以有自己新定义的属性和方法。继承来的属性和方法尽管是隐式的,但仍是类C的属性和方法。
2、继承的好处
??子类可以直接访问父类中的非私有的属性和行为
??提高代码的复用性。
??让类与类之间产生了关系,是多态的前提
3、继承的实例
public class test03 {
public static void main(String[] args) {
Dog dog = new Dog();
System.out.println(dog.color+" "+dog.legs);
dog.color = "黑色";
dog.legs = 4;
System.out.println(dog.color+" "+dog.legs);
/* 1.
* 注意上下文继承而来属性的值,因为在new时构造器给父类的属性赋值,
* 所以子类继承而来的也是同样的值,但子类继承来的值是自己独有一份的,看第三个注释
* */
Dog dog1 = new Dog();
System.out.println(dog1.color+" "+dog1.legs);
dog1.eat();
dog1.sleep();
/* 2.
子类继承了父类的work方法,也是独有一份,但没有重写,
所以其方法的执行指向父类的work方法,详细原理将在文章多态中介绍
*/
dog1.work();
/* 3.
*上文再次new子类(从而再次给父类属性赋值),
*但这里再次调用dog继承而来的属性,值不变,证明继承而来的的确是子类独有一份
*/
System.out.println(dog.color+" "+dog.legs);
/*运行结果
* 黄色 4
黑色 4
黄色 4
狗吃屎
狗睡觉
动物工作
黑色 4
* * */
}
}
class Animal{
int legs;
String color;
Animal(String color,int legs){
this.color = color;//this代表当前对象,谁调用就是谁
this.legs = legs;
}
public void eat(){
System.out.println("动物吃饭");
}
protected void sleep(){
System.out.println("动物睡觉");
}
public void work(){
System.out.println("动物工作");
}
private void method(){
System.out.println("特有方法");
}
}
class Dog extends Animal{
/*
* 当new子类的对象时,子类的构造器会默认会调用父类的构造器
* 就算没写,编译器也会帮我们添加。
* 当父类没有默认的无参构造器时,便会发生编译错误,
*解决这个编译错误有三种办法:
*1.在父类Person中添加默认构造方法,子类Student会隐式调用父类的默认构造方法。
*2.在子类Studen构造方法添加super语句,显式调用父类构造方法,super语句必须是第一条语句。
*3.在子类Studen构造方法添加this语句,显式调用当前对象其他构造方法(this(参数类型 参数...);),
* this语句必须是第一条语句。
* */
Dog(){
super("黄色",4);//super代表父类(可以看作父类对象的引用)
}
@Override
public void eat(){
System.out.println("狗吃屎");
}
/*重写的条件:
* 1.权限修饰符大于等于不包括private
* 2.返回值类型相同
* 3.方法名和参数列表相同
*
* */
@Override
public void sleep(){//public大于protected
System.out.println("狗睡觉");
}
/*私有方法不能继承
@Override
private void method(){
}*/
}
静态代码块、构造代码块,构造方法的执行顺序:
父类静态代码块→子类静态代码块→父类构造代码块→父类构造方法→子类构造代码块→子类构造方法
三、多态
1、多态的概念
??多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。
2、多态的条件
??继承:在多态中必须存在有继承关系的子类和父类
??重写:子类对父类中某些方法进行重新定义
??向上转型:父类的引用指向子类对象
3、多态的表现形式
??方法的重载
??方法的重写
??接口的实现
4、底层的体现
1)方法的重载:方法的重载跟jvm虚拟机中的静态分派密切相关,举个梨子:
public class Test01 {
static class Human{}
static class Man extends Human{}
static class Woman extends Human{}
public void sayhello(Human human){
System.out.println("hello human");
}
public void sayhello(Man man){
System.out.println("hello man");
}
public void sayhello(Woman woman){
System.out.println("hello woman");
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
Test01 test = new Test01();
test.sayhello(man);
test.sayhello(woman);
}
}
/*运行结果
hello human
hello human
* */
??在Human man = new Man();这条代码中,我们将“Human”称为变量”human“的的静态类型,也叫做外观类型,后面的“Man”则是变量的实际类型,两者的区别在于静态类型的变化仅仅在使用时发生(即我们写这条代码的时候),变量本身的静态类型不会被改变,并且最终的静态类型是在编译期确定的。而实际类型变化的结果在运行期确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。
??上面的代码中,test.sayhello(参数)已经确定方法的接收者是test,后面具体使用哪个重载版本,就完全取决于传入参数的数量和类型了,上面的代码其类型都是Human。我们可以看到,在这个过程中,我们是依赖静态类型来定位方法的执行版本的,这样的分派动作称为静态分派,发生在编译期间。也可以称为编译时多态。
2)方法的重写:方法的重写跟jvm虚拟机的动态分派密切相关
,同样的,举个栗子:
public class Test02 {
static class Human{
public void say(){
System.out.println("我是人类");
}
public void specialmethod(){
System.out.println("父类不被重写的方法");
}
}
static class Man extends Human{
@Override
public void say(){
System.out.println("我是男人");
}
public void dowhat(){
System.out.println("男人赚钱养家");
}
}
static class Woman extends Human{
@Override
public void say(){
System.out.println("我是女人");
}
public void dowhat(){
System.out.println("女人相夫教子");
}
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.say();
woman.say();
man.specialmethod();
woman.specialmethod();
//human.dowhat();编译期报错,向上转型会丢失子类特有的方法
Man man1 = (Man)man;//利用向下转型可以弥补向上转型的缺陷
man1.dowhat();
}
}
/*运行结果
* 我是男人
我是女人
父类不被重写的方法
父类不被重写的方法
男人赚钱养家
* */
??导致静态类型同样是Human的两次方法调用结果不同的是两个引用的实际类型不同,jvm虚拟机又是如何根据实际类型来动态分派执行不同的版本的呢?我们通过查看main方法的字节码指令来解释。
??上图中第1行到第八行是new两个对象的过程。第10行跟第12行是方法调用指令,这两条指令是一摸一样的,但是其执行的结果却是不一样的,原因需要从invokevirtual指令的多态查找过程说起,在此之前,让我们先看一下jvm虚拟机中的方法区(jdk8开始,方法区只是一概念,其实现在本地内存中,称为元空间)中子类的虚方法表:
拓展:
??方法表的实现如下:
??父类的方法比子类的方法先得到解析,即父类的方法相比子类的方法位于表的前列。
??表中每项对应于一个方法,索引到实际方法的实现代码上。如果子类重写了父类中某个方法的代码,则该方法第一次出现的位置的索引更换到子类的实现代码上,而不会在方法表中出现新的项。
??JVM 运行时,当代码索引到一个方法时,是根据它在方法表中的偏移量来实现访问的。(第一次执行到调用指令时,会执行解析,将符号索引替换为对应的直接索引)。
??由于 invokevirtual 调用的方法在对应的类的方法表中都有固定的位置,直接索引的值可以用偏移量来表示。(符号索引解析的最终目的是完成直接索引:对象方法和对象变量的调用都是用偏移量来表示直接索引的)
从图中我们可以看出:
??1.子类中从父类继承而来的方法都是自己独享一份的
??2.如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口(这点在图中没有体现)
??3.如果子类中重写了该方法,子类方法表中的地址将会替换成指向子类实现版本的入口。
??4.并且能够看出同样签名的方法在方法表中的偏移量是一样的。这个偏移量仅仅是说Man方法表中的继承自Object类的方法、继承自Human类的方法的偏移量与Object和Human类中的同样方法的偏移量是一样的。与woman是没有什么关系的。
??了解了上面的知识,我们再来解释一下invokevirtual指令的执行过程:
1)根据#9在常量池中得到方法调用的符号引用
2)在Human类的方法表中查找say()方法,如果找到了,则将方法say()在方法表中的索引项目(index) 记录到Human类的常量池中第9个常量表中(常量池解析)。
3)在执行invokevirtual指令前,Man对象或者Woman对象的引用就已将被压入操作数栈中,invokevirtual指令会根据这个引用找到在堆中的对象进而找到在方法区中这个类的虚方法表
4)这时,在通过前面的index找到相应索引位置的方法入口地址,如果这个方法被子类重写了,则指向子类自己的方法代码执行,如果没被重写,则指向父类的方法代码执行,这个时候,相同索引值的好处就体现出来了。
??综上所诉,两次调用的中的相同的符号引用解析到了不同的直接引用上,这个过程就是java语言中重写的本质。
3)接口的实现
(1)在常量池中找到方法调用的符号引用
(2)查看Person的方法表,得到speak方法在该方法表的偏移量(假设为15)。这样就得到该方法的直接引用。
(3)依据this指针确定方法接收者(girl)的实际类型
(4)依据对象的实际类型得到该实际类型相应的方法表,依据偏移量15查看有无重写(override)该方法。假设重写。则能够直接调用;假设没有重写。则须要拿到依照继承关系从下往上的基类(这里是Person类)的方法表。同样依照这个偏移量15查看有无该方法。
3)接口的实现
??接口的实现跟方法重写的情况类似,我们试着把上面方法重写的代码改写成是接口的实现,如下
public class Test03 {
static interface Human{
public void say();
}
static class Man implements Human {
@Override
public void say(){
System.out.println("我是男人");
}
public void dowhat(){
System.out.println("男人赚钱养家");
}
}
static class Woman implements Human {
@Override
public void say(){
System.out.println("我是女人");
}
public void dowhat(){
System.out.println("女人相夫教子");
}
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.say();
woman.say();
//man.dowhat();编译期报错,向上转型会丢失子类特有的方法
Man man1 = (Man)man;//利用向下转型可以弥补向上转型的缺陷
man1.dowhat();
}
/*运行结果
* 我是男人
我是女人
男人赚钱养家
* */
}
再来看一下main方法的字节码指令:
??我们重点看第17跟23行,可以发现,除了方法指令的不同(忽略类名),其他基本没什么不同,那么invokeinterface指令跟invokevirtual指令在执行时有什么不同呢?
??子类实现接口中的方法在方法表中的位置不是固定的,因此不能依照索引值来直接定位,Java 对于接口方法的调用是采用搜索方法表的方式,如,要在Man的方法表中找到say()方法,必须搜索Man的整个方法表。
??因为每次接口调用都要搜索方法表,所以从效率上来说,接口方法的调用总是慢于类方法的调用的。
??
5、多态的应用
??说了怎么多,我们最后来看一下多态应用的一个例子,感受一下多态带给我们的方便!
import java.util.ArrayList;
public class Test04 {
public static void main(String[] args) {
phoneBag phoneBag = new phoneBag();//new一个手机袋
//往手机袋里面放手机
phoneBag.add(new Vivo());
phoneBag.add(new iphone());
phoneBag.add(new OPPO());
//获得手机袋里面的手机数量
System.out.println(phoneBag.getNum());
//利用向下转型运行各个手机的独立功能
Vivo vivo = (Vivo)phoneBag.getPhone(0);
vivo.use();
iphone iphone1 = (iphone)phoneBag.getPhone(1);
iphone1.use();
OPPO oppo = (OPPO)phoneBag.getPhone(2);
oppo.use();
/*运行结果
3
我使用vivo手机拍照
我使用苹果手机玩游戏
我使用OPPO手机看视频
* */
}
}
interface Phone{
}
class Vivo implements Phone{
public void use() {
System.out.println("我使用vivo手机拍照");
}
}
class iphone implements Phone{
public void use() {
System.out.println("我使用苹果手机玩游戏");
}
}
class OPPO implements Phone{
public void use() {
System.out.println("我使用OPPO手机看视频");
}
}
class phoneBag{
ArrayList<Phone> phones = new ArrayList<>();
//往手机袋里装手机
public void add(Phone phone){
phones.add(phone);
}
//获得手机的数量
public int getNum(){
return phones.size();
}
//根据索引取得手机
public Phone getPhone(int index){
return phones.get(index);
}
}
??在上面的例子中,比如我有个土豪,他有很多部手机,各部手机各司其职,我们申明三个手机类,各个手机类有自己的使用方法,并且都实现了手机大类这个接口。除此之外,我们申明了一个手机袋类,在里面new了一个ArrayList集合来存放手机,泛型使用Phone,为什么要使用Phone这个接口作为泛型呢,你想想,我们这里只是三部手机,如果这个土豪有钱没地花,买了20部呢,难道我们要申明20个不同类型的集合来存放他们吗?这样代码量太高了。
??很多时候,我们需要把很多种类的实例对象,全部扔到一个集合。因此我们实现同个接口,用父类的引用将这些实际类型放在同一个集合中,用到的时候再取出来用。经过了这个过程,子类实例已经赋值给了父类引用(即完成了向上转型),但很遗憾的丢失了子类扩展的方法。
??解决这个问题的方式就是向下转型(向下转型前一定要有向上转型),利用向下转型我们就可以使用子类独有的方法。
结语
本文详细地介绍了java的三大特征,篇幅最长的是多态,阅读多态时需要有jvm虚拟机一定的基础。希望能够帮助到大家,也欢迎大家对内容指正。
内容总结
以上是互联网集市为您收集整理的Java三大特征详解全部内容,希望文章能够帮你解决Java三大特征详解所遇到的程序开发问题。 如果觉得互联网集市技术教程内容还不错,欢迎将互联网集市网站推荐给程序员好友。
内容备注
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 gblab@vip.qq.com 举报,一经查实,本站将立刻删除。
内容手机端
扫描二维码推送至手机访问。