栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

JavaSE(1)

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

JavaSE(1)

(一)Java基础语法

JVM:JAVA虚拟机;跨平台;.class二进制文件
JDK和JRE:

  • JDK:java develop kit;包含JRE,同时提供一些开发需要的工具
  • JRE:java runtime environment;最基本的java运行环境,可以用来运行java程序;不同操作系统不同jre


初识java运行机制

会发现,在运行了一次java程序后,左边文件目录会出现一个out目录


发现生成了一个class字节码文件;那这个class文件如何运行呢?在终端中进入out文件的最下面层,输入java 类名,即可

用javac编译Main.java文件就可以编译它,并产生一个class文件


变成class文件后就可以在jvm上运行了
java既是编译型(编译为.class)语言,又是解释型(jvm)语言

学习IDEA的使用
  • 构建项目
  • 运行

在左边选择运行哪个类

  • 调试
    就是运行旁边那个虫子,先打断点;“评估”功能
  • 搜索

    或者双击两次shift键
  • 项目结构
变量和关键字
  • 变量 :

变量和C语言中的变量不同,Java中的变量是存放在JVM管理的内存中,C语言的变量存放在内存(某些情况下需要手动释放内存,而Java会自动帮助我们清理变量占据的内存)Java和C++很类似,但是没有指针!Java也叫C+±-

Java是强类型语言

  • 常量 :
final int a = 10;
a = 10;  //报错!

常量前面必须添加final关键字(C语言里面是const,虽然Java也有,但是不能使用!)
这只是final关键字的第一个用法,后面还会有更多的用法。

  • 注释 :
//TODO 待做标记
基本数据类型

Java中的数据类型分为基本数据类型和引用类型两大类,引用类型我们在面向对象时再提;首先我们需要了解有哪些类型。然后,我们需要知道的,并不是他们的精度如何,能够表示的范围有多大,而是为什么Java会给我们定义这些类型,计算机是怎么表示这些类型的,这样我们才能够更好的记忆他们的精度、表示的范围大小。所以,我们从计算机原理的角度出发,带领大家走进Java的基本数据类型。

计算机中的二进制表示

在Java中,无论是小数还是整数,他们都要带有符号(和C语言不同,C语言有无符号数)所以,首位就作为我们的符号位,还是以4个bit为例,首位现在作为符号位(1代表负数,0代表正数):
最小:1111 => -(22+21+2^0) => -7
最大:0111 => +(22+21+2^0) => +7 => 7
现在,我们4bit能够表示的范围变为了-7~+7,这样的表示方式称为原码。

计算机中的加减法
  • 原码

虽然原码表示简单,但是原码在做加减法的时候,很麻烦!以4bit位为例:

1+(-1) = 0001 + 1001 = 怎么让计算机去计算?(虽然我们知道该去怎么算,但是计算机不知道!)

我们得创造一种更好的表示方式!于是我们引入了反码:

  • 反码

-正数的反码是其本身
-负数的反码是在其原码的基础上, 符号位不变,其余各个位取反

经过上面的定义,我们再来进行加减法:

1+(-1) = 0001 + 1110 = 1111 => -0 (直接相加,这样就简单多了!)

思考:1111代表-0,0000代表+0,在我们实数的范围内,0有正负之分吗?

0既不是正数也不是负数,那么显然这样的表示依然不够合理!

  • 补码

根据上面的问题,我们引入了最终的解决方案,那就是补码,定义如下:
-正数的补码就是其本身 (不变!)
-负数的补码是在其原码的基础上, 符号位不变, 其余各位取反, 最后+1. (即在反码的基础上+1)

其实现在就已经能够想通了,-0其实已经被消除了!我们再来看上面的运算:

1+(-1) = 0001 + 1111 = (1)0000 => +0 (现在无论你怎么算,也不会有-0了!)

所以现在,4bit位能够表示的范围是:-8~+7(Java使用的就是补码!)

整数类型

在Java中,整数类型包括以下几个:
-byte 字节型 (8个bit,也就是1个字节)范围:-128~+127
-short 短整形(16个bit,也就是2个字节)范围:-32768~+32767
-int 整形(32个bit,也就是4个字节)最常用的类型!
-long 长整形(64个bit,也就是8个字节)最后需要添加l或L

long都装不下怎么办?BigInteger!

数字已经达到byte的最大值了,还能加吗?为了便于理解,以4bit为例:
0111 + 0001 = 1000 => -8(你没看错,就是这样!)

字符类型和字符串

在Java中,存在字符类型,它能够代表一个字符:
-char 字符型(16个bit,也就是2字节,它不带符号!)范围是0 ~ 65535
-使用Unicode表示就是:u0000 ~ uffff

字符要用单引号扩起来!比如 char c = ‘淦’;

字符其实本质也是数字,但是这些数字通过编码表进行映射,代表了不同的字符,比如字符’A’的ASCII码就是数字65,所以,char类型其实可以转换为上面的整数类型。

Java的char采用Unicode编码表(不是ASCII编码!),Unicode编码表包含ASCII的所有内容,同时还包括了全世界的语言,ASCII只有1字节,而Unicode编码是2字节,能够代表65536种文字,足以包含全世界的文字了!(我们编译出来的字节码文件也是使用Unicode编码的,所以利用这种特性,其实Java支持中文变量名称、方法名称甚至是类名)

String就是Java中的字符串类型(注意,它是一个类,创建出来的字符串本质是一个对象,不是我们的基本类型)字符串就像它的名字一样,代表一串字符,也就是一句完整的话。

字符串用双引号括起来!比如:String str = “一日三餐没烦恼”;

小数类型

首先来看看Java中的小数类型包含哪些:
-float 单精度浮点型 (32bit,4字节)
-double 双精度浮点型(64bit,8字节)

思考:小数的范围该怎么定义呢?我们首先要了解的是小数在计算机里面是如何存放的:

根据国际标准 IEEE 754,任意一个二进制浮点数 V 可以表示成下面的形式:
V = (-1)^S × M × 2^E
(1)(-1)^S 表示符号位,当 S=0,V 为正数;当 S=1,V 为负数。
(2)M 表示有效数字,大于等于 1,小于 2,但整数部分的 1 不变,因此可以省略。(例如尾数为1111010,那么M实际上就是1.111010,尾数首位必须是1,1后面紧跟小数点,如果出现0001111这样的情况,去掉前面的0,移动1到首位;题外话:随着时间的发展,IEEE 754标准默认第一位为1,故为了能够存放更多数据,就舍去了第一位,比如保存1.0101 的时候, 只保存 0101,这样能够多存储一位数据)
(3)2^E 表示指数位。(用于移动小数点)

比如: 对于十进制的 5.25 对应的二进制为:101.01,相当于:1.0101*2^2。所以,S 为 0,M 为 1.0101,E 为 2。所以,对于浮点类型,最大值和最小值不仅取决于符号和尾数,还有它的阶码。我们在这里就不去计算了,想了解的可以去搜索相关资料。

思考:就算double有64bit位数,但是依然存在精度限制,如果我要进行高精度的计算,怎么办?BigDecimal!

布尔类型

布尔类型占据的空间大小并未明确定义,而是根据不同的JVM会有不同的实现

类型转换 隐式类型转换

隐式类型转换支持字节数小的类型自动转换为字节数大的类型,整数类型自动转换为小数类型,转换规则如下:
byte→short(char)→int→long→float→double

问题:为什么long比float大,还能转换为float呢?小数的存储规则让float的最大值比long还大,只是可能会丢失某些位上的精度!

所以,如下的代码就能够正常运行:

byte b = 9;
short s = b;
int i = s;
long l = i;
float f = l;
double d = f;
System.out.println(d);

//输出 9.0
显示类型转换

显示类型转换也叫做强制类型转换,也就是说,违反隐式转换的规则,牺牲精度强行进行类型转换。

int i = 128;
byte b = (byte)i;
System.out.println(b);

//输出 -128

为什么结果是-128?精度丢失了!

int 类型的128表示:00000000 00000000 00000000 10000000
byte类型转换后表示:xxxxxxxx xxxxxxxx xxxxxxxx 10000000 => -128

数据类型自动提升

在参与运算时(也可以位于表达式中时,自增自减除外),所有的byte型、short型和char的值将被提升到int型:

byte b = 105;
b = b + 1;   //报错!
System.out.println(b);

这个特性是由 Java虚拟机规范 定义的,也是为了提高运行的效率。其他的特性还有:
-如果一个操作数是long型,计算结果就是long型
-如果一个操作数是float型,计算结果就是float型
-如果一个操作数是double型,计算结果就是double型

运算符 赋值和算术运算符

需要注意的是,+还可以用作字符串连接符使用:

System.out.println("lbw" + "nb");  //lbwnb

当然,字符串可以直接连接其他类型,但是会全部当做字符串处理:

int a = 7, b = 15;
System.out.println("lbw" + a + b); //lbw715
位运算符
& //按位与
| //按位或
^ //按位异或 0 ^ 0 = 0
~ //按位非

三目运算符
int a = 7, b = 15;
String str = a > b ? "行" : "不行";  // 判断条件(只能是boolean,或返回boolean的表达式) ? 满足的返回值 : 不满足的返回值 
System.out.println("汉堡做的行不行?"+str);  //汉堡做的行不行?不行

流程控制 选择结构

在上述语句中,只有判断主体等于case后面的值时,才会执行case中的语句,同时需要使用break来跳出switch语句,否则会继续向下运行!

为什么switch效率更高呢,因为switch采用二分思想进行查找(这也是为什么switch只能判断值相等的原因),能够更快地找到我们想要的结果!

循环结构 (二)Java对象和多态 面向对象基础

所有的对象,都需要通过new关键字创建,基本数据类型不是对象!Java不是纯面对对象语言!

不是基本类型的变量,都是引用类型,引用类型变量代表一个对象,而基本数据类型变量,保存的是基本数据类型的值,我们可以通过引用来对对象进行操作。(最好不要理解为引用指向对象的地址,初学者不要谈内存,学到JVM时再来讨论)

对象占用的内存由JVM统一管理,不需要手动释放内存,当一个对象不再使用时(比如失去引用或是离开了作用域)会被JVM自动清理,内存管理更方便!

类的基本结构

我们自己创建一个自己的类,创建的类文件名称应该和类名一致。

成员变量

在类中,可以包含许多的成员变量,也叫成员属性,成员字段(field)通过.来访问我们类中的成员变量,我们可以通过类创建的对象来访问和修改这些变量。成员变量是属于对象的!

public class Student {
    String name;
    int age;
}

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Student s = new Student();
        s.name = "lbxx";
        System.out.println(s.name);
    }
}

成员变量默认带有初始值,也可以自己定义初始值。

成员方法

成员方法和成员变量一样,是属于对象的,只能通过对象去调用!

传入方法的参数,如果是基本类型,会在调用方法的时候,对参数的值进行复制,方法中的参数变量,不是我们传入的变量本身!

传入方法的参数,如果是引用类型,那么传入的依然是该对象的引用!(类似于C语言的指针)

public class A{
    B modify(B b) {
        b.name = "abc";
        return b;
    }
}

public class B{
    String name;
}

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        B b = new B();
        b.name = "lbw";
        B bb = new A().modify(b);
        System.out.println(bb == b);        // true
    }
}

方法的重载

一个类中可以包含多个同名的方法,但是需要的形式参数不一样。
方法的返回类型,可以相同,也可以不同,但是仅返回类型不同,是不允许的!

public class Test {
    int a(){   //原本的方法
       return 1;
    }

    int a(int i){  //ok,形参不同
        return i;
    }
    
    void a(byte i){  //ok,返回类型和形参都不同
        
    }
    
    void a(){  //错误,仅返回值类型名称不同不能重载
        
    }
}

现在我们就可以使用不同的参数,但是支持调用同样的方法,执行一样的逻辑:

public class Test {
    int sum(int a, int b){   //只有int支持,不灵活!
        return a+b;
    }
    
    double sum(double a, double b){  //重写一个double类型的,就支持小数计算了
        return a+b;
    }
}

构造方法

反编译(将一个class文件又转换成java代码)一个空的类A


发现多了一个东西

构造方法(构造器)没有返回值,也可以理解为,返回的是当前对象的引用!每一个类都默认自带一个无参构造方法。

//反编译结果
package com.test;

public class Test {
    public Test() {    //即使你什么都不编写,也自带一个无参构造方法,只是默认是隐藏的
    }
}

反编译其实就是把我们编译好的class文件变回Java源代码。

Test test = new Test();  //实际上存在Test()这个的方法,new关键字就是用来创建并得到引用的
// new + 你想要使用的构造方法

这种方法没有写明返回值,但是每个类都必须具有这个方法!只有调用类的构造方法,才能创建类的对象!

类要在一开始准备的所有东西,都会在构造方法里面执行,完成构造方法的内容后,才能创建出对象!

一般最常用的就是给成员属性赋初始值

我们可以手动指定有参构造,当遇到名称冲突时,需要用到this关键字

public class Student {
    String name;

    Student(String name){   //形参和类成员变量冲突了,Java会优先使用形式参数定义的变量!
        this.name = name;  //通过this指代当前的对象属性,this就代表当前对象
    }
}

//idea 右键快速生成!

注意,this只能用于指代当前对象的内容,因此,只有属于对象拥有的部分才可以使用this,也就是说,只能在类的成员方法中使用this,不能在静态方法中使用this关键字。

在我们定义了新的有参构造之后,默认的无参构造会被覆盖!

如果同时需要有参和无参构造,那么就需要用到方法的重载!手动再去定义一个无参构造。

public class Student {
    String name;

    Student(){

    }

    Student(String name){
        this.name = name;
    }
}

成员变量的初始化始终在构造方法执行之前

public class Student {
    String a = "sadasa";

    Student(){
        System.out.println(a);
    }

    public static void main(String[] args) {
        Student s = new Student();		// sadasa
    }
}

静态变量和静态方法

静态变量和静态方法是类具有的属性(后面还会提到静态类、静态代码块),也可以理解为是所有对象共享的内容。我们通过使用static关键字来声明一个变量或一个方法为静态的,一旦被声明为静态,那么通过这个类创建的所有对象,操作的都是同一个目标,也就是说,对象再多,也只有这一个静态的变量或方法。那么,一个对象改变了静态变量的值,那么其他的对象读取的就是被改变的值。

不推荐使用对象来调用,被标记为静态的内容,可以直接通过类名.xxx的形式访问

public static void main(String[] args) {
   Student.a = 10;
   System.out.println(Student.a);
}

简述类加载机制 :
类并不是在一开始就全部加载好,而是在需要时才会去加载(提升速度)以下情况会加载类:
-访问类的静态变量,或者为静态变量赋值
-new 创建类的实例(隐式加载)
-调用类的静态方法
-子类初始化时
-其他的情况会在讲到反射时介绍

所有被标记为静态的内容,会在类刚加载的时候就分配,而不是在对象创建的时候分配,所以说静态内容一定会在第一个对象初始化之前完成加载。

public class Student {
    static int a = test();  //直接调用静态方法,只能调用静态方法

    Student(){
        System.out.println("构造类对象");
    }

    static int test(){   //静态方法刚加载时就有了
        System.out.println("初始化变量a");
        return 1;
    }
}



思考:下面这种情况下,程序能正常运行吗?如果能,会输出什么内容?

public class Student {
    static int a = test();

    static int test(){
        return a;
    }

    public static void main(String[] args) {
        System.out.println(Student.a);		// 0
    }
}

定义和赋值是两个阶段,在定义时会使用默认值(上面讲的,类的成员变量会有默认值)定义出来之后,如果发现有赋值语句,再进行赋值,而这时,调用了静态方法,所以说会先去加载静态方法,静态方法调用时拿到a,而a这时仅仅是刚定义,所以说还是初始值,最后得到0

代码块和静态代码块

代码块在对象 创建时执行,也是属于类的内容,但是它在构造方法执行之前执行(和成员变量初始值一样),且每创建一个对象时,只执行一次!(相当于构造之前的准备工作)

public class Student {
    {
        System.out.println("我是代码块");
    }

    Student(){
        System.out.println("我是构造方法");
    }
}

静态代码块和上面的静态方法和静态变量一样,在类 刚加载(只调用这一次)时就会调用;

public class Student {
    static int a;

    static {
        a = 10;
    }
    
    public static void main(String[] args) {
        System.out.println(Student.a);
    }
}

String和StringBuilder类
String str = "abdcwdew";
System.out.println(str.toUpperCase());      // str.toUpperCase().sout 快捷方式

不是基本数据类型,也就是引用类型,如果使用==来判断,判断的是它们是不是同一个对象;这里由于底层优化的问题,结果会是true(当然最好用equals方法判断两个字符串的值的是否相当);这里实际上str1和str2还是两个对象

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        String str1 = "abc";
        String str2 = "abc";
        System.out.println(str1 == str2);       // true
    }
}

字符串类是一个比较特殊的类,他是Java中唯一重载运算符的类!(Java不支持运算符重载,String是特例)

String的对象直接支持使用+或+=运算符来进行拼接,并形成新的String对象!(String的字符串是不可变的!)

String a = "dasdsa", b = "dasdasdsa";
String l = a+b;
System.out.println(l);

大量进行字符串的拼接似乎不太好,编译器是很聪明的,String的拼接有可能会被编译器优化为StringBuilder来减少对象创建(对象频繁创建时很费时间同时占内存的!)

String result="String"+"and"; //会被优化成一句!String result = "Stringand";

String str1="String";
String str2="and";
String result=str1+str2;
//变量随时可变,在编译时无法确定result的值,那么只能在运行时再去确定

String str1="String";
String str2="and";
String result=(new StringBuilder(String.valueOf(str1))).append(str2).toString();
//使用StringBuilder,会采用类似于第一种实现,显然会更快!

StringBuilder也是一个类,但是它能够存储可变长度的字符串!

StringBuilder builder = new StringBuilder();
builder
       .append("a")
       .append("bc")
       .append("d");   //链式调用
String str = builder.toString();
System.out.println(str);

包和访问控制 包声明和导入


包其实就是用来区分类位置的东西,也可以用来将我们的类进行分类,类似于C++中的namespace!

package com.test;

public class Test{
  
}

包其实是文件夹,比如com.test就是一个com文件夹中包含一个test文件夹,再包含我们Test类。

一般包按照个人或是公司域名的规则倒过来写 顶级域名.一级域名.二级域名 com.java.xxxx

如果需要使用其他包里面的类,那么我们需要import(类似于C/C++中的include)

import com.test.Student;

也可以导入包下的全部(一般导入会由编译器自带帮我们补全,但是一定要记得我们需要导包!)

import com.test.*

Java默认为我们导入了以下的包,不需要去声明

import java.lang.*

静态导入

静态导入可以直接导入某个类的静态方法或者是静态变量,导入后,相当于这个方法或是类在定义在当前类中,可以直接调用该方法。

静态导入不会进行类的初始化!(比如static静态代码块)(前面说的类的加载的情况

package com.test.util;

public class Test {
    public static void fun() {

    }
}

package com.test;

import static com.test.util.Test.fun;

public class Main {
    public static void main(String[] args) {
        fun();
    }
}

访问控制

Java支持对类属性访问的保护,也就是说,不希望外部类访问类中的属性或是方法,只允许内部调用,这种情况下我们就需要用到权限控制符。

权限控制符可以声明在方法、成员变量、类前面,一旦声明private,只能类内部访问!

public class Student {
    private int a = 10;   //具有私有访问权限,只能类内部访问
}

public static void main(String[] args) {
    Student s = new Student();
    System.out.println(s.a);  //还可以访问吗?		// 不能
}

和文件名称相同的类,只能是public,并且一个java文件中只能有一个public class!

// Student.java
public class Student {
    
}
class Test{   //不能添加权限修饰符!只能是default
	
}

数组类型

数组是相同类型数据的有序集合。数组可以代表任何相同类型的一组内容(包括引用类型和基本类型)

int[] arr = new int[10];  //需要new关键字来创建!
String[] arr2 = new String[10];

数组本身也是类(编程不可见,C++写的),不是基本数据类型!

int[] arr = new int[10];
System.out.println(arr.length);   //数组有成员变量!
System.out.println(arr.toString());   //数组有成员方法!

一维数组
类型[] 变量名称 = new 类型[数组大小];
类型 变量名称n = new 类型[数组大小];  //支持C语言样式,但不推荐!

类型[] 变量名称 = new 类型[]{...};  //静态初始化(直接指定值和大小)
类型[] 变量名称 = {...};   //同上,但是只能在定义时赋值

创建出来的数组每个元素都有默认值(规则和类的成员变量一样,C语言创建的数组需要手动设置默认值),我们可以通过下标去访问

我们可以通过数组变量名称.length来获取当前数组长度

数组在创建时,就固定长度,不可更改!访问超出数组长度的内容,会出现错误!

数组做实参,因为数组也是类,所以形参得到的是数组的引用而不是复制的数组,操作的依然是数组对象本身

public static void main(String[] args) {
    int[] arr = new int[]{1, 2, 3};
    test(arr);
    System.out.println(arr[0]);
}

private static void test(int[] arr){
    arr[0] = 2934;
}

数组的遍历

foreach

int[] arr = new int[]{1, 2, 3};
for (int i : arr) {
    System.out.println(i);
}

二维数组

二维数组其实就是存放数组的数组,每一个元素都存放一个数组的引用,也就相当于变成了一个平面。

//三行两列
int[][] arr = { {1, 2},
                {3, 4},
                {5, 6}};
System.out.println(arr[2][1]);

可变长参数

可变长参数其实就是数组的一种应用,我们可以指定方法的形参为一个可变长参数,要求实参可以根据情况动态填入0个或多个,而不是固定的数量

由于是数组,所以说只能使用一种类型的可变长参数,并且可变长参数只能放在最后一位!

public static void main(String[] args) {
     test("AAA", "BBB", "CCC");    //可变长,最后都会被自动封装成一个数组
}
    
private static void test(String... test){
     System.out.println(test[0]);    //其实参数就是一个数组
}

实战:三大基本排序算法
  • 冒泡排序
    冒泡排序就是冒泡,其实就是不断使得我们无序数组中的最大数向前移动,经历n轮循环逐渐将每一个数推向最前
  • 插入排序
    插入排序其实就跟我们打牌是一样的,我们在摸牌的时候,牌堆是乱序的,但是我们一张一张摸到手中进行排序,使得它变成了有序的
  • 选择排序
    选择排序其实就是每次都选择当前数组中最大的数排到最前面
封装、继承和多态

封装、继承和多态是面向对象编程的三大特性。

封装

封装的目的是为了保证变量的安全性,使用者不必在意具体实现细节,而只是通过外部接口即可访问类的成员,如果不进行封装,类中的实例变量可以直接查看和修改,可能给整个代码带来不好的影响,因此在编写类时一般将成员变量私有化,外部类需要同getter和setter方法来查看和设置变量。

设想:学生小明已经创建成功,正常情况下能随便改他的名字和年龄吗?

public class Student {
    private String name;
    private int age;
  
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }
}

也就是说,外部现在只能通过调用我定义的方法来获取成员属性,而我们可以在这个方法中进行一些额外的操作,比如小明可以修改名字,但是名字中不能包含"小"这个字。

public void setName(String name) {
    if(name.contains("小")) return;
    this.name = name;
}

单独给外部开放设置名称的方法,因为我还需要做一些额外的处理,所以说不能给外部直接操作成员变量的权限!

封装思想其实就是把实现细节给隐藏了,外部只需知道这个方法是什么作用,而无需关心实现。

封装就是通过访问权限控制来实现的。

继承

在定义不同类的时候存在一些相同属性,为了方便使用可以将这些共同属性抽象成一个父类,在定义其他子类时可以继承自该父类,减少代码的重复定义,子类可以使用父类中非私有的成员。

现在学生分为两种,艺术生和体育生,他们都是学生的分支,但是他们都有自己的方法:

public class SportsStudent extends Student{   //通过extends关键字来继承父类

    public SportsStudent(String name, int age) {
        super(name, age);   //必须先通过super关键字(指代父类),实现父类的构造方法!
    }

    public void exercise(){
        System.out.println("我超勇的!");
    }
}

public class ArtStudent extends Student{

    public ArtStudent(String name, int age) {
        super(name, age);
    }

    public void art(){
        System.out.println("随手画个毕加索!");
    }
}

子类具有父类的全部属性,protected可见但外部无法使用(包括private属性,不可见,无法使用),同时子类还能有自己的方法。继承只能继承一个父类,不支持多继承!

每一个子类必须定义一个实现父类构造方法的构造方法,也就是需要在构造方法开始使用super(),如果父类使用的是默认构造方法,那么子类不用手动指明。

所有类都默认继承自Object类,除非手动指定类型,但是依然改变不了最顶层的父类是Object类。所有类都包含Object类中的方法,比如:

public static void main(String[] args) {
Object obj = new Object;
System.out.println(obj.hashCode());  //求对象的hashcode,默认是对象的内存地址
System.out.println(obj.equals(obj));  //比较对象是否相同,默认比较的是对象的内存地址,也就是等同于 ==
System.out.println(obj.toString());  //将对象转换为字符串,默认生成对象的类名称+hashcode
}

关于Object类的其他方法,我们会在Java多线程中再来提及。

多态

多态是同一个行为具有多个不同表现形式或形态的能力。也就是同样的方法,由于实现类不同,执行的结果也不同!

方法的重写 :

我们之前学习了方法的重载,方法的重写和重载是不一样的,重载是原有的方法逻辑不变的情况下,支持更多参数的实现,而重写是直接覆盖原有方法!

//父类中的study
public void study(){
    System.out.println("学习");
}

//子类中的study
@Override  //声明这个方法是重写的,但是可以不要,我们现阶段不接触
public void study(){
    System.out.println("给你看点好康的");
}

再次定义同样的方法后,父类的方法就被覆盖!子类还可以给父类方法提升访问权限!

public static void main(String[] args) {
     SportsStudent student = new SportsStudent("lbw", 20);
     student.study();   //输出子类定义的内容
}

思考:静态方法能被重写吗?
答 :不能,因为静态方法是类所有的

当我们在重写方法时,不仅想使用我们自己的逻辑,同时还希望执行父类的逻辑(也就是调用父类的方法)怎么办呢?

public void study(){
    super.study();
    System.out.println("给你看点好康的");
}

同理,如果想访问父类的成员变量,也可以使用super关键字来访问,注意,子类可以具有和父类相同的成员变量!而在方法中访问的默认是 形参列表中 > 当前类的成员变量 > 父类成员变量

public void setTest(int test){
    test = 1;
  	this.test = 1;
  	super.test = 1;
}

再谈类型转换 :
我们曾经学习过基本数据类型的类型转换,支持一种数据类型转换为另一种数据类型,而我们的类也是支持类型转换的(仅限于存在亲缘关系的类之间进行转换)比如子类可以直接向上转型:

Student student = new SportsStudent("lbw", 20);  //父类变量引用子类实例
student.study();     //得到依然是具体实现的结果,而不是当前类型的结果

我们也可以把已经明确是由哪个类实现的父类引用,强制转换为对应的类型:

Student student = new SportsStudent("lbw", 20);  //是由SportsStudent进行实现的
//... do something...

SportsStudent ps = (SportsStudent)student;  //让它变成一个具体的子类
ps.sport();  //调用具体实现类的方法

这样的类型转换称为向下转型。

instanceof关键字 :
那么我们如果只是得到一个父类引用,但是不知道它到底是哪一个子类的实现怎么办?我们可以使用instanceof关键字来实现,它能够进行类型判断!

private static void test(Student student){
    if (student instanceof SportsStudent){
        SportsStudent sportsStudent = (SportsStudent) student;
        sportsStudent.sport();
    }else if (student instanceof ArtStudent){
        ArtStudent artStudent = (ArtStudent) student;
        artStudent.art();
    }
}

通过进行类型判断,我们就可以明确类的具体实现到底是哪个类!

思考:student instanceof Student的结果是什么?
答 :true;因为SportsStudent继承自Student(也就是说instance of,如果是子类,依然会返回真

再谈final关键字 :
我们目前只知道final关键字能够使得一个变量的值不可更改,那么如果在类前面声明final,会发生什么?

public final class Student {   //类被声明为终态,那么它还能被继承吗
  	
}

类一旦被声明为终态,将无法再被继承,不允许子类的存在!而方法被声明为final呢?

public final void study(){  //还能重写吗		// final方法不能被重写了
    System.out.println("学习");
}

如果类的成员属性被声明为final,那么必须在构造方法中或是在定义时赋初始值!

private final String name = "abc";   //引用类型不允许再指向其他对象;即使是定义为null,后面也不能再改了;一共只能被赋值一次
// name = "bcd"; 报错
private final int age;    //基本类型值不允许发生改变

public Student(String name, int age) {
    this.name = name;
    this.age = age;
}

学习完封装继承和多态之后,我们推荐在不会再发生改变的成员属性上添加final关键字,JVM会对添加了final关键字的属性进行优化!

抽象类

类本身就是一种抽象,而抽象类,把类还要抽象,也就是说,抽象类可以只保留特征,而不保留具体呈现形态,比如方法可以定义好,但是我可以不去实现它,而是交由子类来进行实现!

public abstract class Student {    //抽象类
		public abstract void test();  //抽象方法
}

通过使用abstract关键字来表明一个类是一个抽象类,抽象类可以使用abstract关键字来表明一个方法为抽象方法,也可以定义普通方法,抽象方法不需要编写具体实现(无方法体)但是必须由子类实现(除非子类也是一个抽象类)!

抽象类由于不是具体的类定义,因此无法直接通过new关键字来创建对象!

AbstractStudent s = new AbstractStudent(){    //只能直接创建带实现的匿名内部类!
  public void test(){
    
  }
}

因此,抽象类一般只用作继承使用!抽象类使得继承关系之间更加明确:

public void study(){   //现在只能由子类编写,父类没有定义,更加明确了多态的定义!同一个方法多种实现!
    System.out.println("给你看点好康的");
}

接口

接口甚至比抽象类还抽象,他只代表某个确切的功能!也就是只包含方法的定义,甚至都不是一个类!接口包含了一些列方法的具体定义,类可以实现这个接口,表示类支持接口代表的功能(类似于一个插件,只能作为一个附属功能加在主体上,同时具体实现还需要由主体来实现)

public interface Eat {
	void eat(); 
}

通过使用interface关键字来表明是一个接口(注意,这里class关键字被替换为了interface)接口只能包含public权限的抽象方法!(Java8以后可以有默认实现)我们可以通过声明default关键字来给抽象方法一个默认实现:

public interface Eat {
    default void eat(){
        //do something...
    }
}

接口中定义的变量,默认为public static final

public interface Eat {
    int a = 1;
    void eat();
}

一个类可以实现很多个接口,但是不能理解为多继承!(实际上实现接口是附加功能,和继承的概念有一定出入,顶多说是多继承的一种替代方案)一个类可以附加很多个功能!

public class SportsStudent extends Student implements Eat, ...{
		@Override
    public void eat() {
        
    }
}

类通过implements关键字来声明实现的接口!每个接口之间用逗号隔开!

实现接口的类也能通过instanceof关键字判断(实现则为true),也支持向上和向下转型!

内部类

类中可以存在一个类!各种各样的长相怪异的代码就是从这里开始出现的!

成员内部类

我们的类中可以在嵌套一个类:

public class Test {
    class Inner{   //类中定义的一个内部类
        
    }
}

成员内部类和成员变量和成员方法一样,都是属于对象的,也就是说,必须存在外部对象,才能创建内部类的对象!

public static void main(String[] args) {
    Test test = new Test();
    Test.Inner inner = test.new Inner();   //写法有那么一丝怪异,但是没毛病!
}

静态内部类

静态内部类其实就和类中的静态变量和静态方法一样,是属于类拥有的,我们可以直接通过类名.去访问:

public class Test {
    static class Inner{

    }
}

public static void main(String[] args) {
    Test.Inner inner = new Test.Inner();   //不用再创建外部类对象了!
}

局部内部类

对,你没猜错,就是和局部变量一样哒~

public class Test {
    public void test(){
        class Inner{

        }
        
        Inner inner = new Inner();
    }
}

匿名内部类

匿名内部类才是我们的重点,也是实现lambda表达式的原理!匿名内部类其实就是在new的时候,直接对接口或是抽象类的实现:

public static void main(String[] args) {
        Eat eat = new Eat() {
            @Override
            public void eat() {
                //DO something...
            }
        };
    }

我们不用单独去创建一个类来实现,而是可以直接在new的时候写对应的实现!但是,这样写,无法实现复用,只能在这里使用!

lambda表达式

读作λ表达式,它其实就是我们接口匿名实现的简化,比如说:

public static void main(String[] args) {
        Eat eat = new Eat() {
            @Override
            public void eat() {
                //DO something...
            }
        };
    }

public static void main(String[] args) {
        Eat eat = () -> {};   //等价于上述内容
    }

(参数) {返回值}

因此,如果接口中有一个以上的方法,就不支持lambda表达式了

lambda表达式(匿名内部类)只能访问外部的final类型或是隐式final类型的局部变量!

为了方便,JDK默认就为我们提供了专门写函数式的接口,这里只介绍Consumer

枚举类

假设现在我们想给小明添加一个状态(跑步、学习、睡觉),外部可以实时获取小明的状态:

public class Student {
    private final String name;
    private final int age;
    private String status;
  
  	//...
  
  	public void setStatus(String status) {
        this.status = status;
    }

    public String getStatus() {
        return status;
    }
}

但是这样会出现一个问题,如果我们仅仅是存储字符串,似乎外部可以不按照我们规则,传入一些其他的字符串。这显然是不够严谨的!

有没有一种办法,能够更好地去实现这样的状态标记呢?我们希望开发者拿到使用的就是我们定义好的状态,我们可以使用枚举类!

public enum Status {
    RUNNING, STUDY, SLEEP    //直接写每个状态的名字即可,分号可以不打,但是推荐打上
}

使用枚举类也非常方便,我们只需要直接访问即可

public class Student {
    private final String name;
    private final int age;
    private Status status;
  
 		//...
  
  	public void setStatus(Status status) {   //不再是String,而是我们指定的枚举类型
        this.status = status;
    }

    public Status getStatus() {
        return status;
    }
}

public static void main(String[] args) {
    Student student = new Student("小明", 18);
    student.setStatus(Status.RUNNING);
    System.out.println(student.getStatus());
}

枚举类型使用起来就非常方便了,其实枚举类型的本质就是一个普通的类,但是它继承自Enum类,我们定义的每一个状态其实就是一个public static final的Status类型成员变量!

// Compiled from "Status.java"
public final class com.test.Status extends java.lang.Enum {
  public static final com.test.Status RUNNING;
  public static final com.test.Status STUDY;
  public static final com.test.Status SLEEP;
  public static com.test.Status[] values();
  public static com.test.Status valueOf(java.lang.String);
  static {};
}

既然枚举类型是普通的类,那么我们也可以给枚举类型添加独有的成员方法

public enum Status {
    RUNNING("睡觉"), STUDY("学习"), SLEEP("睡觉");   //无参构造方法被覆盖,创建枚举需要添加参数(本质就是调用的构造方法!)

    private final String name;    //枚举的成员变量
    Status(String name){    //覆盖原有构造方法(默认private,只能内部使用!)
        this.name = name;
    }
  
  	public String getName() {   //获取封装的成员变量
        return name;
    }
}

public static void main(String[] args) {
    Student student = new Student("小明", 18);
    student.setStatus(Status.RUNNING);
    System.out.println(student.getStatus().getName());
}

枚举类还自带一些继承下来的实用方法

Status.valueOf("")   //将名称相同的字符串转换为枚举
Status.values()   //快速获取所有的枚举		// .for来循环输出

基本类型包装类

Java并不是纯面向对象的语言,虽然Java语言是一个面向对象的语言,但是Java中的基本数据类型却不是面向对象的。在学习泛型和集合之前,基本类型的包装类是一定要讲解的内容!

我们的基本类型,如果想通过对象的形式去使用他们,Java提供的基本类型包装类,使得Java能够更好的体现面向对象的思想,同时也使得基本类型能够支持对象操作!

  • byte -> Byte
  • boolean -> Boolean
  • short -> Short
  • char -> Character
  • int -> Integer
  • long -> Long
  • float -> Float
  • double -> Double

包装类实际上就行将我们的基本数据类型,封装成一个类(运用了封装的思想)

private final int value;   //Integer内部其实本质还是存了一个基本类型的数据,但是我们不能直接操作

public Integer(int value) {
    this.value = value;
}

现在我们操作的就是Integer对象而不是一个int基本类型了!

public static void main(String[] args) {
     Integer i = 1;   //包装类型可以直接接收对应类型的数据,并变为一个对象!
     System.out.println(i + i);    //包装类型可以直接被当做一个基本类型进行操作!
}

自动装箱和拆箱

那么为什么包装类型能直接使用一个具体值来赋值呢?其实依靠的是自动装箱和拆箱机制

Integer i = 1;    //其实这里只是简写了而已
Integer i = Integer.valueOf(1);  //编译后真正的样子

调用valueOf来生成一个Integer对象!

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)   //注意,Java为了优化,有一个缓存机制,如果是在-128~127之间的数,会直接使用已经缓存好的对象,而不是再去创建新的!(面试常考)
       return IntegerCache.cache[i + (-IntegerCache.low)];
  	return new Integer(i);   //返回一个新创建好的对象
}

而如果使用包装类来进行运算,或是赋值给一个基本类型变量,会进行自动拆箱:

public static void main(String[] args) {
    Integer i = Integer.valueOf(1);
    int a = i;    //简写
    int a = i.intValue();   //编译后实际的代码
  
  	long c = i.longValue();   //其他类型也有!
}

既然现在是包装类型了,那么我们还能使用==来判断两个数是否相等吗?

public static void main(String[] args) {
    Integer i1 = 28914;
    Integer i2 = 28914;

    System.out.println(i1 == i2);   //实际上判断是两个对象是否为同一个对象(内存地址是否相同)
    System.out.println(i1.equals(i2));   //这个才是真正的值判断!
}

注意IntegerCache带来的影响!

package com.test;

public class Main {
    public static void main(String[] args) {
        Integer i1 = 100;
        Integer i2 = 100;
        System.out.println(i1 == i2);       // true
    }
}

public static void main(String[] args) {
    Integer i1 = 28914;
    Integer i2 = 28914;

    System.out.println(i1+1 == i2+1);		// true 因为自动被拆箱了(基本运算)
}

面向对象编程实战 对象设计(面向对象、多态运用)
  • 设计一个Person抽象类,包含吃饭运动学习三种行为,分为工人、学生、老师三种职业。
  • 设计设计一个接口考试,只有老师和学生会考试。
  • 设计一个方法,模拟让人类进入考场,要求只有会考试的人才能进入,并且考试。
package Person;

public class Main {
    public static void main(String[] args) {

    }

    private static void test(Person person) {
        if (person instanceof Exam) {
            Exam e = (Exam)person;
            e.exam();
        }
    }
}

package Person;

public abstract class Person {
    public abstract void eat();
    public abstract void run();
    public abstract void study();
}

package Person;

public class Student extends Person implements Exam{
    @Override
    public void eat() {

    }

    @Override
    public void run() {

    }

    @Override
    public void study() {

    }

    @Override
    public void exam() {

    }
}

package Person;

public class Worker extends Person implements Exam{
    @Override
    public void eat() {

    }

    @Override
    public void run() {

    }

    @Override
    public void study() {

    }

    @Override
    public void exam() {

    }
}

package Person;

public interface Exam {
    void exam();
}

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/915527.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号