专注于 JetBrains IDEA 全家桶,永久激活,教程
持续更新 PyCharm,IDEA,WebStorm,PhpStorm,DataGrip,RubyMine,CLion,AppCode 永久激活教程

《Java核心技术》第10版读书总结(一)

java是什么

Java是由Sun Microsystems(后来被Oracle收购)公司于1995年5月推出的Java面向对象程序设计语言和Java平台的总称,是一个高级程序设计语言。

Java的主要特性

  • 简单性
  • 面向对象
  • 分布式
  • 健壮性
  • 安全性
  • 体系中立性
  • 可移植性
  • 解释型
  • 高性能
  • 多线程
  • 动态性

Java的主要的应用场景

  • web后端
  • Android app开发

Java各种术语介绍

术语名 缩写 解释
Java Development kit JDK 编写Java程序的程序员使用的软件
Java Runtime Environment JRE 运行Java程序所必须的环境
Server JRE 运行于服务器上的JRE
Standard Edition SE java标准版,主要学习这个
Enterprise Edition EE 用于服务器应用,现在不常用
Micro Edition ME 用于手机和其他小型设备的Java平台,由于洛基亚手机破产后,现在不常用
Java FX 用于图形化界面的一个替代工具包
OpenJDK java se一个免费开源版本

java有关的问题

Java和JavaScript有什么关系?

javaScript是一种在网页上使用的脚本语言,和Java没什么关系。

Java是解释型还是编译型

先了解下解释型和编译型的定义

定义:
编译型语言:把做好的源程序全部编译成二进制代码的可运行程序。然后,可直接运行这个程序。
解释型语言:把做好的源程序翻译一句,然后执行一句,直至结束
区别:
编译型语言,执行速度快、效率高;依靠编译器、跨平台性差些。
解释型语言,执行速度慢、效率低;依靠解释器、跨平台性好。

java是解释型的语言,因为虽然java也需要编译,编译成.class文件,但是并不是机器可以识别的语言,而是字节码, 最终还是需要 jvm的解释,才能在各个平台执行,这同时也是java跨平台的原因。所以可是说java即是编译型的,也是解释型, 但是假如非要归类的话,从概念上的定义,恐怕java应该归到解释型的语言中。

JIT是什么

JIT是Java即时编译器,通过把”热点代码”(即反复被使用的代码)编译成与本地平台相关的机器码,用来提高运行速度

jdk jre jvm是什么,以及它们之间的关系

jdk : jdk是Java开发工具包

jre : jre是Java运行环境。没有jre,java程序就不能运行。

jvm : jvm是java虚拟机,负责执行编译后的.class文件

简单的说:jdk包括jre,jre包括jvm。具体的区别可以看这篇博客

java的安装(windows版)

点击进入Oracle的官网下载Java8 sdk,下载完成后进行安装.其他版本安装看这里

Java目录文件介绍

安装好Java后,打开Java文件,目录结构如下

61_1.png

如图有两个文件jdk1.8.0_151,jre1.8.0_151。其中1.8.0_151是版本号。jdkjre在上一篇博客中介绍过,分别是 java开发工具包和Java运行环境。

打开jdk1.8.0_151文件夹

61_2.png

目录介绍:

  • bin :最主要的是编译器(javac.exe);
  • db :jdk从1.6之后内置了Derby数据库,它是一个纯用Java实现的内存数据库。用Java实现的, 所以可以在任何平台上运行;另外一个特点是体积小,免安装,只需要几个小jar包就可以运行了。
  • include :java和JVM交互用的头文件;
  • lib :常用类库
  • jre :java运行环境
  • src.zip : 源码文件
  • javafx-src.zip : javafx源码文件
  • 其他 :配置文件

为什么有两个jre

之所以需要两套 jre ,是因为可以分担工作;当用户只需要执行 java 的程序时,那么c:\program files\java\jre下的 jre 就 ok !当用户是 java 程序员,需要 java 开发环境,那么开发时,系统就会优先去找“ java 、 javac ”这些命令本身的目录或者他们的父目录的 jre ;这样开发时一般运行的是 jdk 下的 jre ;而运行外部程序时,一般运行的是c:\program files\java\jre,实现了分工,不同的 jre 负责各自范围的内容.回答转自这里

配置环境变量

右击我的电脑->属性->高级系统设置->环境变量->系统变量

61_3.png

61_4.png

61_5.png

在”系统变量”中设置3项属性

变量名 : JAVA_HOME
变量值 : C:\Program Files\Java\jdk1.8.0_151 //根据自己Java的安装目录 作用 : 设置Java所在的路径

变量名 : CLASSPATH 变量值 :.;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar; //记得前面有个”.” 作用 : classpath变量的含义是告诉jvm要使用或执行的class放在什么路径上,便于JVM加载class文件,.;表示当前路径,tools.jar和dt.jar为类库路径。 注意:jdk1.5以上不需要这个配置.

变量名 : Path 变量值 :往已有的Path路径后添加 %JAVA_HOME%\bin;%JAVA_HOME%\jre\bin; 作用 : path变量的含义就是系统在任何路径下都可以识别java,javac命令.

验证环境变量是否配置成功

打开命令行键入命令: java -versionjavajavac几个命令, 出现以下信息,说明环境变量配置成功;

61_6.png

Java简单的命令介绍

  • java -version : 显示当前Java的版本
  • javac 类名 : javac程序是一个编译器,将.java文件编译成.class文件.
  • java 类名 : java程序启动Java虚拟机,虚拟机执行编译器放在class文件中的字节码

使用IDE(集成开发环境)

IntelliJ IDEA是目前最好的Java IDE,具体的安装教程可以看这里

一个简单的Java程序

public class Main {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

上面是一个简单的Java程序,作用是在控制台输出Hello World!。下面来分析这段代码

public

public是访问修饰符,这些修饰符用于控制程序的其他部分对这段代码的访问级别。访问 修饰符包括public private protect和默认修饰符.关于访问修饰符,后面会具体介绍。

class Main

class是关键字,class 类名用来定义一个类,Java程序的所有内容都必须放在类中。

Java类名的规则:

1、 类名不能以数字开头,必须是字母
2、 不能使用Java保留字(后面会介绍Java保留字)

Java类名的规范:(注意,规则是强制要求的,下面规范不是。不过遵守规范有利于代码结构清晰,建议遵守规范)

1、 类名是以大写首字母开头的名词,如First
2、 如果类名由多个单词组成,则每个单词首字母都应该大写(即驼峰命名法),如FirstSimple

public static void main(Strinf[] args){…}

这个是在类Main中定义了一个方法。static是指静态,void表示无返回值,main是方法名(后面的文章会介绍)。注意:main 方法是一个特殊的方法,每一个Java应用程序都必须有一个main方法,没有main方法程序将不能执行,而且main方法的修饰符必须是public

java的基本数据类型

java是一种强类型语言,这意味着必须给每一个变量声明一种类型。在Java中一共有八种基本类型,它们分别是 整形(byte,short,int,long),浮点型(float,double),字符型(char),表示真值的(boolean)

整形

整形表示没有小数部分的值,它允许是负数.java有四种整形,具体区别如下:

类型 储存需求 取值范围
int 4字节 -2,147,483,648(-2^31)~ 2,147,483,647(2^31 – 1)(正好超过20亿)
short 2字节 -32768(-2^15)~ 32767(2^15 – 1)
byte 1字节 -128(-2^7)~ 127(2^7-1)
long 8字节 -9,223,372,036,854,775,808(-2^63)~ 9,223,372,036,854,775,807(2^63 -1)

注意:long类型后面通常有Ll(l容易和1混淆,所以推荐使用大写的L),如2000000000000000L

二进制,八进制,十六进制

二进制 : 加上前缀0b0B表示二进制,如0b1001表示9

八进制 : 加上前缀0表示八进制,如010表示8,不过八进制表示法容易混淆,所以不推荐使用

十六进制 : 加上前缀0x0X表示十六进制.

从Java 7 开始,还可以为数字字面量加下划线,如用1_000_000表示一百万.这些下划线只为了让人更 易读,Java编译器会除去这些下划线。

浮点型

浮点型用于表示有小数部分数值的类型,在Java中有两种浮点类型,具体的区别如下:

类型 储存需求 取值范围
float(单精度浮点值) 4节字 float 数据类型是单精度、32位、符合IEEE 754标准的浮点数;
double(双精度浮点值) 8字节 double 数据类型是双精度、64 位、符合IEEE 754标准的浮点数

float类型的数值有一个后缀fF,如果没有后缀的浮点值默认为double(也可以往浮点值后面加dD,表示double类型)

特殊的浮点数值 :

  • 正无穷大
  • 负无穷大
  • NaN 表示0/0或负数的平方根,不是一个数字

注意:不能用浮点型做精确运算;更不能使用while( i == 浮点数)作为循环条件,由于二进制本身的特性。

char

char类型的值用”表示,如char a = 'a'.具体的关于char的说明请看后面的文章

char类型特殊字符的转义序列

转义序列 名称 Unicode值
\b 退格 \u0008
\t 制表 \u0009
\n 换行 \u000a
\r 回车 \u000d
双引号 \u0022
单引号 \u0027
\ 反斜杠 \u005c
\u2122
π \u03c0

boolean类型

boolean(布尔)类型有两个值truefalse.用来判断逻辑条件

String

String 类有 11 种构造方法,这些方法提供不同的参数来初始化字符串,下面是常用的:

//直接创建
String str1 = "这个字符串";
//使用字符串创建
String str2 = new String("这个字符串");
//使用数组
char[] helloArray = { 'r', 'u', 'n', 'o', 'o', 'b'};
String helloString = new String(helloArray);  

...

当n个字符串直接由+连接起来组成新的字符串时,不是分别创建n+1个String对象。而是直接将合成一个字符串,即只创建了一个字符串对象

String a = "abc"+"123"+"567";

当分别创建字符串变量,然后使用+连接起来时

        String a = "abc";
        String a1 = "123";
        String b1 = a+a1;

        //会转化为
        String a = "abc";
        String a1 = "123";
        (new StringBuilder()).append(a).append(a1).toString();

注意:String是不可变字符串,如果需要对字符串做很多修改,那么应该选择使用 StringBufferStringBuilder

String的常用方法

length()

返回字符串的长度

concat

连接字符串,例如string1.concat(string2);

char charAt(int index)

返回指定索引处的 char 值。

boolean endsWith(String suffix)

测试此字符串是否以指定的后缀结束。

indexOf(…)

int indexOf(int ch)
返回指定字符在此字符串中第一次出现处的索引。

int indexOf(int ch, int fromIndex)
返回在此字符串中第一次出现指定字符处的索引,从指定的索引开始搜索。

int indexOf(String str)
返回指定子字符串在此字符串中第一次出现处的索引。

int indexOf(String str, int fromIndex)
返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始。

intern()

转载这里

  今天在看一本书的时候注意到一个String的intern()方法,平常没用过,只是见过这个方法,也没去仔细看过这个方法。所以今天看了一下。个人觉得给String类中加入这个方法可能是为了提升一点点性能,因为从常量池取数据比从堆里面去数据要快一些。(个人感觉)

  API上的那几句关于这个方法,其实总结一句就是调用这个方法之后把字符串对象加入常量池中,常量池我们都知道他是存在于方法区的,他是方法区的一部分,而方法区是线程共享的,所以常量池也就是线程共享的,但是他并不是线程不安全的,他其实是线程安全的,他仅仅是让有相同值的引用指向同一个位置而已,如果引用值变化了,但是常量池中没有新的值,那么就会新开辟一个常量结果来交给新的引用,而并非像线程不同步那样,针对同一个对象,new出来的字符串和直接赋值给变量的字符串存放的位置是不一样的,前者是在堆里面,而后者在常量池里面,另外,在做字符串拼接操作,也就是字符串相”+”的时候,得出的结果是存在在常量池或者堆里面,这个是根据情况不同不一定的,我写了几行代码测试了一下。

先上结果:

1、 直接定义字符串变量的时候赋值,如果表达式右边只有字符串常量,那么就是把变量存放在常量池里面。
2、 new出来的字符串是存放在堆里面。
3、 对字符串进行拼接操作,也就是做”+”运算的时候,分2中情况:

i. 表达式右边是纯字符串常量,那么存放在栈里面。
ii. 表达式右边如果存在字符串引用,也就是字符串对象的句柄,那么就存放在堆里面。

    String str1 = "aaa";
        String str2 = "bbb";
        String str3 = "aaabbb";
        String str4 = str1 + str2;
        String str5 = "aaa" + "bbb";
        System.out.println(str3 == str4); // false
        System.out.println(str3 == str4.intern()); // true
        System.out.println(str3 == str5);// true

结果:str1、str2、str3、str5都是存在于常量池,str4由于表达式右半边有引用类型,所以str4存在于堆内存,而str5表达式右边没有引用类型,是纯字符串常量,就存放在了常量池里面。

lastIndexOf(…)

int (int ch)
 返回指定字符在此字符串中最后一次出现处的索引。
int lastIndexOf
int lastIndexOf(int ch, int fromIndex)
返回指定字符在此字符串中最后一次出现处的索引,从指定的索引处开始进行反向搜索。

int lastIndexOf(String str)
返回指定子字符串在此字符串中最右边出现处的索引。

int lastIndexOf(String str, int fromIndex)
返回指定子字符串在此字符串中最后一次出现处的索引,从指定的索引开始反向搜索。

boolean matches(String regex)

告知此字符串是否匹配给定的正则表达式

replaceXXX()


String replace(char oldChar, char newChar) 返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的。 String replaceAll(String regex, String replacement) 使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串。 String replaceFirst(String regex, String replacement) 使用给定的 replacement 替换此字符串匹配给定的正则表达式的第一个子字符串。

split()

转载菜鸟教程

split() 方法根据匹配给定的正则表达式来拆分字符串。
注意: . 、 | 和 * 等转义字符,必须得加 \。
注意:多个分隔符,可以用 | 作为连字符。

语法:

public String[] split(String regex, int limit)
参数
regex -- 正则表达式分隔符。
limit -- 分割的份数。
返回值
字符串数组。

实例:

public class Test {
    public static void main(String args[]) {
        String str = new String("Welcome-to-Runoob");

        System.out.println("- 分隔符返回值 :" );
        for (String retval: str.split("-")){
            System.out.println(retval);
        }

        System.out.println("");
        System.out.println("- 分隔符设置分割份数返回值 :" );
        for (String retval: str.split("-", 2)){
            System.out.println(retval);
        }

        System.out.println("");
        String str2 = new String("www.runoob.com");
        System.out.println("转义字符返回值 :" );
        for (String retval: str2.split("\\.", 3)){
            System.out.println(retval);
        }

        System.out.println("");
        String str3 = new String("acount=? and uu =? or n=?");
        System.out.println("多个分隔符返回值 :" );
        for (String retval: str3.split("and|or")){
            System.out.println(retval);
        }
    }
}

以上程序执行结果为:

- 分隔符返回值 :
Welcome
to
Runoob

- 分隔符设置分割份数返回值 :
Welcome
to-Runoob

转义字符返回值 :
www
runoob
com

多个分隔符返回值 :
acount=?
 uu =?
 n=?

String trim()

返回字符串的副本,忽略前导空白和尾部空白。

substring(…)

String substring(int beginIndex)
返回一个新的字符串,它是此字符串的一个子字符串。

String substring(int beginIndex, int endIndex)
返回一个新字符串,它是此字符串的一个子字符串。

更多Java String的方法见api

数组

声明数组变量

首先必须声明数组变量,才能在程序中使用数组。下面是声明数组变量的语法:

dataType[] arrayRefVar;   // 推荐

dataType arrayRefVar[];  // 效果相同,但不是首选方法

注意: 建议使用 dataType[] arrayRefVar 的声明风格声明数组变量。 dataType arrayRefVar[] 风格是来自 C/C++ 语言 , 在Java中采用是为了让 C/C++ 程序员能够快速理解java语言。

double[] myList;         // 首选的方法

double myList[];         //  效果相同,但不是首选方法

创建数组

Java语言使用new操作符(后面会介绍)来创建数组,语法:arrayRefVar = new dataType[arraySize];

上面的语法语句做了两件事:

  • 使用 dataType[arraySize] 创建了一个数组。
  • 把新创建的数组的引用赋值给变量 arrayRefVar。
//数组变量的声明,和创建数组可以用一条语句完成,如下所示:
dataType[] arrayRefVar = new dataType[arraySize];
//另外,你还可以使用如下的方式创建数组。
dataType[] arrayRefVar = {value0, value1, ..., valuek};

数组的元素是通过索引访问的。数组索引从 0 开始,所以索引值从 0 到 arrayRefVar.length-1。

数组的拷贝

//错误示例,这种方式两个数组变量将引用同一个数组
int[] a1 = new int[3];
int[] a2 = a1

//正确示例
int[] a3 = Arrays.copyOf(a1,a1.length);

注释

在Java中有三种标记注释的方式

//单行注释

/*
 *多行注释
 */

 /**
  * Java doc,这里的注释会生成文档
  *
  */

变量

声明变量

在Java中声明变量的语法是类型 变量名,例如

int a;
boolean A;//Java是大小写敏感的
float c;
double m,n;//不推荐

注意:每个变量声明都必须以;结尾

变量初始化

int a = 1;//直接初始化
int b;
b = 3;//对已经声明过的变量进行初始化

注意:声明一个变量之后,必须进行初始化,不能使用未初始化的值,否则会报错。(成员变量声明后有默认值,已经初始化了,所以可以直接使用)

常量

在Java中,利用关键字final指示常量(常量是指不会变的量),例如:

final int A = 10;//必须直接赋值,赋值后就不能再更改,习惯上常量名全大写

运算符

算术运算符

操作符 描述 例子
+ 加法 a + b
减法 a – b
* 乘法 a * b
/ 除法 a / b
取余 a % b
++ 自增 a++++a两种,具体区别见下文
自减 a----a两种,具体区别见下文

a++++a的区别:

++a:先进行自增运算,再进行表达式运算,而a++先进行表达式运算,再进行自增运算.(a----a类似)

        int a = 1;
        int b = 1;
        System.out.println(a++);//先执行println方法,再执行a++,输出结果为 1
        System.out.println(++b);//先执行b++,再执行println方法,输出结果为 2

关系运算符

运算符 描述
== 检查如果两个操作数的值是否相等,如果相等则条件为真。
!= 检查如果两个操作数的值是否相等,如果值不相等则条件为真。
> 检查左操作数的值是否大于右操作数的值,如果是那么条件为真。
< 检查左操作数的值是否小于右操作数的值,如果是那么条件为真。
>= 检查左操作数的值是否大于或等于右操作数的值,如果是那么条件为真。
<= 检查左操作数的值是否小于或等于右操作数的值,如果是那么条件为真。

逻辑运算符

操作符 描述
&& 当且仅当两个操作数都为真,条件才为真。
|| 如果任何两个操作数任何一个为真,条件为真。
用来反转操作数的逻辑状态。如果条件为true,则逻辑非运算符将得到false。

位移运算符

操作符 描述
与运算
| 或运算
按位取反运算符
<< 按位左移运算符
>> 按位右移运算符
>>> 按位右移补零操作符

赋值运算符

操作符 描述 例子
= 赋值运算符
+= 加和赋值操作符 C += A等价于C = C + A
-= 减和赋值操作符 C -= A等价于C = C-A
* = 乘和赋值操作符 C *= A等价于C = C * A
/= 除和赋值操作符 C / = A等价于C = C / A

以上只列举了比较常用的运算符,具体的运算符可以看菜鸟教程

java循环结构

while循环

while循环的基本形式:

while(布尔表达式){
   //循环语句,只要布尔表达式一直为true,循环就会一直执行下去
}

do…while循环

基本形式:

do{
  //循环语句,只要布尔表达式一直为true,循环就会一直执行下去
}while(布尔表达式);

do...while循环和while循环的区别?

do...while循环中的语句至少会执行一次,而while循环里面的语句至少执行0次

for循环

例如:

for (int i = 0; i < 10; i++) {
    System.out.println(i);
}

首先i = 0与10比较,其结果为true,执行System.out.println(i);语句;之后执行i++,此时i = 1,再与10进行比较,直到i < 10的结果为false为止.

增强 for 循环

示例:

int a[] = {1,2,3,4,5};
for (int c : a) {
        System.out.println(c);
}

break和continue

break

break主要用在循环语句或者 switch 语句中,用来跳出整个语句块。

break的使用

break;  //是跳出里层循环(针对几层循环而言))

//示例如下,下面这段代码将一直循环运行
        while (true){
            for (int i = 0; i < 20 ; i++) {
                if (i == 3)
                    break ;
                System.out.println(i);
            }
        }

break name; //跳出指定名字的循环

//示例如下,输出结果为 0 1 2

name:while (true){
            for (int i = 0; i < 20 ; i++) {
                if (i == 3)
                    break name;
                System.out.println(i);
            }
    }

continue

continue 适用于任何循环控制结构中。作用是让程序立刻跳转到下一次循环的迭代。

  • 在 for 循环中,continue 语句使程序立即跳转到更新语句。
  • 在 while 或者 do…while 循环中,程序立即跳转到布尔表达式的判断语句。
  • continue也可以指定循环的名字,直接跳到该循环

条件语句

//if语句
if(布尔表达式)
{
   //如果布尔表达式为true将执行的语句
}

//if...else语句
if(布尔表达式){
   //如果布尔表达式的值为true
}else{
   //如果布尔表达式的值为false
}

//if...else if...else语句
if(布尔表达式 1){
   //如果布尔表达式 1的值为true执行代码
}else if(布尔表达式 2){
   //如果布尔表达式 2的值为true执行代码
}else if(布尔表达式 3){
   //如果布尔表达式 3的值为true执行代码
}else {
   //如果以上布尔表达式都不为true执行代码
}

switch

基本语法

switch(expression){
    case value1 :
       //语句
       break; //可选
    case value2 :
       //语句
       break; //可选
    case value3 :
       //语句
       break; //可选
    //你可以有任意数量的case语句
    default : //可选
       //语句
}

注意:

  • expression变量类型可以是: byte、short、int 或者 char。从 Java SE 7 开始,switch 支持字符串 String 类型了,同时 case 标签必须为字符串常量或字面量。
  • 如果expression与任意case中的值匹配,就相当于打开了开关,之后的值不会再比较而是直接运行语句,直到碰到过break.

示例:

     char grade = 'C';

      switch(grade)
      {
         case 'A' :
            System.out.println("优秀"); 
            break;
         case 'B' :
         case 'C' :
            System.out.println("良好");
            break;
         case 'D' :
            System.out.println("及格");
            break;
         case 'F' :
            System.out.println("你需要再努力努力");
            break;
         default :
            System.out.println("未知等级");
      }

类和对象

类(class)是构造对象的模板或蓝图。可以把类比作,而对象比作一本本具体的书,比如课本,小说等。类是抽象的概念,对象是具体的实体。

对象的具体特征

  • 对象的行为 —— 可以对对象施加哪些操作,或可以对对象施加哪些方法
  • 对象的状态 —— 当施加哪些方法时,对象应该任何响应
  • 对象标识 —— 如何辨别具有相同行为而状态不同的对象

封装

封装是将数据和行为组合在一个类中,并对对象的使用者隐藏了数据的实现方式。可以想象你在自动取款机中取钱的情形,你并不知道自动取款机是如何取出你所需要的钱的,你也不需要知道。当自动取款机的取款的原理发生改变时(可能原来的有安全隐患),但你所要做的仍然是输入取款金额,然后取钱。这就是封装的好处。

一个Java的类

public class People {

    public String name;

    public int age;

    public String adress;

    public void sleep(){
        System.out.println("睡觉");
    }

    public void work(){
        System.out.println("工作");
    }

}

如上示例是一个表示人的Java类,下面我们来分析这个类:

首先class 类名表示一个类,这个上一篇文章介绍过,不用多说。public是公有的意思,即所以类都看。在这个类中我们定义了两个属性nameage,这个也叫做成员变量。 我们又定义了人的行为sleepwork,这个在Java中叫做方法或函数.

现在我们创建了这个模板,那么怎么创建具体的人的对象,这就要用到new,具体代码如下:

 public static void main(String[] args) {
        People people = new People();//获取对象的引用
        people.name = "小明";//对象使用 . 运算符来调用对象的方法或引用成员变量
        people.age = 18;
        people.sleep();
        people.work();
    }

Private

下面我们来扩展这个Java类,我们给它再添加一个是否有女朋友的属性,代码如下:

  public class People {

     String name;

     int age;

     boolean isHaveGirlFriend;

    public void sleep(){
        System.out.println("睡觉");
    }

    public void work(){
        System.out.println("工作");
    }

}

试想一下,如果你问陌生人是否有女朋友,帅哥还好,要是一个肥仔(比如我)就不想理你;但是你可以直接调用 people.isHaveGirlFriend来看我是否有女朋友,这就有点侵犯隐私了。可是我们怎么限制别人对对象的调用(即把成员变量声明为私有域) ,这就要使用private关键字,private关键字修饰的属性或方法只能再本类中可见,一般所有的成员变量要用private修饰.(之后会详细介绍Java 访问修饰符)

public class People {

     String name;

     int age;

     private boolean isHaveGirlFriend;

    public void sleep(){
        System.out.println("睡觉");
    }

    public void work(){
        System.out.println("工作");
    }

    public boolean isHaveGirlFriend() {//通过调用方法来访问属性,可以控制属性的访问,方法名一般取set/get+成员变量名,boolean变量用is+变量名,注意首字母大写
        return isHaveGirlFriend;
    }

    public void setHaveGirlFriend(boolean haveGirlFriend) {
        isHaveGirlFriend = haveGirlFriend;
    }
}

同理方法用private修饰是私有方法

静态域和静态方法

静态域

有时候我们会想要使用一个概念,而不是一个具体的对象来做一些事情。比如要统计人类的总数,那就必须针对人类这个抽象概念,并且 要有属于这个抽象的变量(如人的总数这个变量),而不是每个对象的成员变量(比如说每个人的年龄,你不能说人类的年龄)。要实现这个 抽象的变量(即静态域),只需添加static关键字

public class People {

     String name;

     int age;

     private static int count = 0;//声明一个静态域,表示人类的总数,尽量使用private

     private boolean isHaveGirlFriend;

    public void sleep(){
        System.out.println("睡觉");
    }

    public void work(){
        System.out.println("工作");
    }

    public boolean isHaveGirlFriend() {
        return isHaveGirlFriend;
    }

    public static void add(){
        count++;
    }

    public void setHaveGirlFriend(boolean haveGirlFriend) {
        isHaveGirlFriend = haveGirlFriend;
    }
}

每个对象对于类的成员变量都有自己的拷贝,比如1000个人类对象,就有1000个成员变量name,但是只有一个静态域人类的总数count

注意:静态域只能通过类名.静态域变量名来直接访问,所有也可以称为类域

静态常量

静态常量即可以直接通过类名.常量名直接访问的量,格式如下:

public static final int COUNT = 1;

静态方法

静态方法是只能通过类名.静态方法名调用的方法;静态方法的定义和静态域一样,只需要加上一个static即可。

注意:静态方法只能访问类中的静态域,而不是类的成员变量

方法

方法的定义访问修饰符 返回值 方法名(参数类型 参数名 ...){ }

public void sleep(){System.out.println("睡觉");}//修饰符为public   void表示无返回值  sleep是方法名 System.out.println("睡觉");是方法执行的功能

public String getName(int id){//这是个通过指定参数返回用户姓名的方法 String表示返回参数类型  int id 是方法的参数
        return names[id]; // return 关键字表示返回指定参数  names[id]是返回的参数
}

注意:return会结束所在的方法,例如

public void sleep()
    {
        if (true)
        return;//直接退出方法
        System.out.println("睡觉");//永远无法访问
    }

局部变量

  • 局部变量声明在方法、构造方法或者语句块中;
  • 局部变量在方法、构造方法、或者语句块被执行的时候创建,当它们执行完成后,变量将会被销毁;
  • 访问修饰符不能用于局部变量;
  • 局部变量只在声明它的方法、构造方法或者语句块中可见;
  • 局部变量是在栈上分配的。
  • 局部变量没有默认值,所以局部变量被声明后,必须经过初始化,才可以使用。

成员变量(实例变量)

  • 实例变量声明在一个类中,但在方法、构造方法和语句块之外;
  • 当一个对象被实例化之后,每个实例变量的值就跟着确定;
  • 实例变量在对象创建的时候创建,在对象被销毁的时候销毁;
  • 访问修饰符可以修饰实例变量;
  • 实例变量对于类中的方法、构造方法或者语句块是可见的。一般情况下应该把实例变量设为私有。通过使用访问修饰符可以使实例变量对子类可见;
  • 实例变量具有默认值。数值型变量的默认值是0,布尔型变量的默认值是false,引用类型变量的默认值是null。变量的值可以在声明时指定,也可以在构造方法中指定;
  • 实例变量可以直接通过变量名访问

重载

如果多个方法有相同的名字,不同的参数,那这多个方法重载。例如:

public boolean equal(int a,int b){
        return a == b;
    }

    public boolean equal(String a,String b){
        return b.equals(a);
    }
    //这个方法与上面的方法不能重载,由于返回类型不能作为重载判断的方式
    public int equal(String a,String b){
        return a.compareTo(b);
    }

注意:Java允许重载任何方法。因此要完整地描述一个方法,需要指出方法名以及参数类型,这叫做方法 的签名。返回类型不是方法签名的一部分。

构造器

public 类名(参数...){

}

构造器的语法:

  • 构造器与类名相同
  • 每个类可以有一个以上的构造器,如在类中没有显示的声明一个构造器,系统就会提供一个无参数的构造器
  • 构造器可以有0个,1个或多个参数
  • 构造器没有返回值
  • 构造器总是伴随着new操作一起调用
  • 构造器中不能定义与成员变量同名的局部变量

注意:仅当类没有提供任何构造器的时候,系统才会提供一个默认的构造器。如果自己给出了一个构造器,要想使用系统默认的构造器就必须自己 提供一个默认构造器


public People(){//默认构造器 } public People(String name){ }

this

this关键字表示这个对象的引用

作用1:引用方法的隐式参数

public People(String name){
   this.name = name;//this.name表示成员变量name,name表示构造方法中的name
}

作用2:调用构造器

 public People(){//默认构造器
    this("无名氏");//调用下面的构造器
}

public People(String name){
    this.name = name;
}

对象变量(或引用变量)

我们已经知道People people1 = new People();是获取一个对象的引用;实际上一个对象变量有没有包含一个对象,而仅仅是 引用一个对象。在Java中任何对象变量的值都是对储存在另一个地方的一个对象的引用。new操作符的返回值也是一个引用,如图:

61_7.png

注意:People people;没有引用对象

方法参数

按值调用 : 表示方法接受的是调用者提供的值,Java是按值调用 按引用调用 : 表示方法接受的是调用者提供的变量地址

Java中方法参数的使用情况:

  • 一个方法不能修改一个基本类型的参数
  • 一个方法可以改变一个对象参数的状态
  • 一个方法不能让对象参数引用一个新的对象

在定义一个类时,要给类取名字,类名一般要根据类的作用来取,而不是毫无意义的字符;不过随着定义的类 越来越多,类名很有可能重复。为了解决这个问题,我们就采用包(package)

类的导入

我们使用别的包中的公有类时,我们有两种方式来访问:第一种是通过每个类前面直接添加完整的包名。如:

java.time.LocalDate localDate = java.time.LocalDate.now();

第二种是导入类,导入类的关键字import,语法如下

import 包名.类名;//导入单个类

import 包名.*;//导入这个包中所有的文件

例如:

import java.time.LocalDate;
...
//之后就可以直接使用LocalDate类了
LocalDate localDate = LocalDate.now();

注意:如果一个类中出现重名的类,编译器无法确定使用哪个;那就必须在每一个(重名的)类名 前面加上完整的包名

静态导入

import不仅可以导入类,还可以导入静态方法和静态域

例如

import static java.lang.System.*;//之后就可以直接使用System的静态方法和静态变量

将类放入包中

要想将一个类放入包中,就必须将包的名字放在源文件的开头,包中定义类的代码之前。例如:

package com.demo.core;

public class People{
    ...
}

包的作用域

如果类或类中变量,方法没有被privatepublic修饰,那么这个部分(类,方法和变量)可以被同一包中所有方法访问

类设计技巧

  • 一定要保证数据私有
  • 一定要对数据初始化
  • 不要在类中使用过多的基本类型
  • 不是所有的域都需要独立的域访问器和域修改器
  • 将职责过多的类进行分解
  • 类名和方法名要能够体现它们的职责
  • 优先使用不可变的类

类,超类和子类

判断继承关系

如果类和另一个类之间存在is-a(什么是什么)关系,比如动物,金鱼等。那么就可以使用继承

定义子类

关键字extends表示正在构造的子类继承(或派生)于一个以存在的类。这个以存在的类叫做超类(常用),基类父类;新类 称为子类(常用),派生类孩子类。例如:

格式:

class 父类 {
}

class 子类 extends 父类 {
}

示例:

public class Animal {

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void eat(){
        System.out.println(name+"正在吃东西");
    }

}

public class Dog extends Animal {
    private String master;//狗的主人

    public String getMaster() {
        return master;
    }

    public void setMaster(String master) {
        this.master = master;
    }
}

public static void main(String[] args) {
        Dog dog = new Dog();
        dog.setName("阿福");
        dog.setMaster("小明");
        dog.eat();
    }

从示例可以看到尽管Dog类没有显式地定义setName()getName()等方法,但是属于Animal的类 却可以使用它们,这是因为Dog自动继承了超类Animal中这些方法(只能继承被publicprotect修饰的字段)。

子类具有父类当中的属性和方法,子类就不会存在重复的代码,维护性也提高,代码也更加简洁,提高代码的复用性(复用性主要是可以多次使用, 不用再多次写同样的代码)

注意:Java只支持单继承

覆盖方法

如果我们对父类中已有的方法不满意,就可以自己覆盖父类的方法。例如:

public class Dog extends Animal {
    ...

    @Override
    public void eat() {//覆盖父类的方法,其他不变;程序执行结果为:小明给阿福吃牛排
        System.out.println(master+"给"+getName()+"吃牛排");
    }
}

注意:在覆盖一个方法时,子类的方法不能低于超类的可见性。特别是,如果超类的方法为public,子类方法 一定要声明为public;覆盖时的返回值可以从父类型改为对应的子类型。

super

super是一个指示编译器调用超类方法的特殊关键字。示例:

public class Dog extends Animal {
    ...

    @Override
    public String getName() {
        return "小狗名字是:"+super.getName();//super.getName()调用父类Animal的方法
    }
}

子类构造器

package five;

public class Animal {

    ...

    //给Animal加一个的构造器
    public Animal(String name) {
        this.name = name;
    }
}

public class Dog extends Animal {

    ...

    //子类构造器
    public Dog(String name,String master) {
        super(name);
        this.master = master;
    }
}

由于Dog类的构造器不能访问Animal的私有域,所有必须利用Animal类的构造器对这部分私有域进行 初始化,我们可以通过super关键字实现对超类构造器的调用。使用super调用构造器的语句必须是子类 构造器的第一条语句

如果子类构造器没有显式地调用超类的构造器,则自动地调用超类默认的(不带参数的)构造器。如果超类没有默认 的构造器,并且在子类的构造器中又没有显式地调用超类的其他构造器,则Java编译器报告错误。

多态

转载菜鸟教程

在Java中,对象变量是多态的,一个Animal变量既可以引用一个Dog类对象,也可以引用一个Animal类的任何 一个子类的对象。

当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,再去调用子类的同名方法。 多态的好处:可以使程序有良好的扩展,并可以对所有类的对象进行通用处理。 以下是一个多态实例的演示,详细说明请看注释:

public class Test {
    public static void main(String[] args) {
      show(new Cat());  // 以 Cat 对象调用 show 方法
      show(new Dog());  // 以 Dog 对象调用 show 方法

      Animal a = new Cat();  // 向上转型  
      a.eat();               // 调用的是 Cat 的 eat
      Cat c = (Cat)a;        // 向下转型  
      c.work();        // 调用的是 Cat 的 work
  }  

    public static void show(Animal a)  {
      a.eat();  
        // 类型判断
        if (a instanceof Cat)  {  // 猫做的事情 
            Cat c = (Cat)a;  
            c.work();  
        } else if (a instanceof Dog) { // 狗做的事情 
            Dog c = (Dog)a;  
            c.work();  
        }  
    }  
}

abstract class Animal {  
    abstract void eat();  
}  

class Cat extends Animal {  
    public void eat() {  
        System.out.println("吃鱼");  
    }  
    public void work() {  
        System.out.println("抓老鼠");  
    }  
}  

class Dog extends Animal {  
    public void eat() {  
        System.out.println("吃骨头");  
    }  
    public void work() {  
        System.out.println("看家");  
    }  
}
/*
执行以上程序,输出结果为:
吃鱼
抓老鼠
吃骨头
看家
吃鱼
抓老鼠
*/

注意:不能将一个超类的引用赋给子类变量;但是在Java中,子类数组的引用可以转为超类数组的引用,而不需要采用强制类型转换

继承中方法的调用

61_8.png

61_9.png

注意:上面流程图中判断方法类型中判断private方法是在dog.setName()方法中可能调用了private方法,调用这个private方法时要重新走这个流程所以要判断。

每次调用方法都要进行搜索,时间开销大。因此,虚拟机预先为每一个类创建了一个方法表,其中列出了所有方法的签名和实际调用的方法。这样一来 ,在真正调用这个方法时,虚拟机只查这个表就行。

final类和方法

为类加上final,该类就无法被继承;为方法加上final,该方法就无法被覆写

instanceof

instanceof是用来在运行时指出对象是否是特定类的一个实例,instanceof通过返回一个布尔值来指出,这个对象是否是这个特定类或者是它的子类的一个实例

if(dog instanceof Animal){//如果 dog = null,会返回false
    dog.eat();
}

抽象类

转载菜鸟教程

在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。 抽象类除了不能实例化对象之外,类的其它功能依然存在,成员变量、成员方法和构造方法的访问方式和普通类一样。 由于抽象类不能实例化对象,所以抽象类必须被继承,才能被使用。也是因为这个原因,通常在设计阶段决定要不要设计抽象类。 父类包含了子类集合的常见的方法,但是由于父类本身是抽象的,所以不能使用这些方法。 在Java中抽象类表示的是一种继承关系,一个类只能继承一个抽象类,而一个类却可以实现多个接口。

抽象类的定义

在Java语言中使用abstract class来定义抽象类。如下实例:

/* 文件名 : Employee.java */
public abstract class Employee
{
   private String name;
   private String address;
   private int number;
   public Employee(String name, String address, int number)
   {
      System.out.println("Constructing an Employee");
      this.name = name;
      this.address = address;
      this.number = number;
   }
   public double computePay()
   {
     System.out.println("Inside Employee computePay");
     return 0.0;
   }
   public void mailCheck()
   {
      System.out.println("Mailing a check to " + this.name
       + " " + this.address);
   }
   public String toString()
   {
      return name + " " + address + " " + number;
   }
   public String getName()
   {
      return name;
   }
   public String getAddress()
   {
      return address;
   }
   public void setAddress(String newAddress)
   {
      address = newAddress;
   }
   public int getNumber()
   {
     return number;
   }
}

注意到该 Employee 类没有什么不同,尽管该类是抽象类,但是它仍然有 3 个成员变量,7 个成员方法和 1 个构造方法。 现在如果你尝试如下的例子:

/* 文件名 : AbstractDemo.java */
public class AbstractDemo
{
   public static void main(String [] args)
   {
      /* 以下是不允许的,会引发错误 */
      Employee e = new Employee("George W.", "Houston, TX", 43);

      System.out.println("\n Call mailCheck using Employee reference--");
      e.mailCheck();
    }
}

当你尝试编译AbstractDemo类时,会产生如下错误:

Employee.java:46: Employee is abstract; cannot be instantiated
      Employee e = new Employee("George W.", "Houston, TX", 43);
                   ^
1 error

继承抽象类

我们能通过一般的方法继承Employee类:

/* 文件名 : Salary.java */
public class Salary extends Employee
{
   private double salary; //Annual salary
   public Salary(String name, String address, int number, double
      salary)
   {
       super(name, address, number);
       setSalary(salary);
   }
   public void mailCheck()
   {
       System.out.println("Within mailCheck of Salary class ");
       System.out.println("Mailing check to " + getName()
       + " with salary " + salary);
   }
   public double getSalary()
   {
       return salary;
   }
   public void setSalary(double newSalary)
   {
       if(newSalary >= 0.0)
       {
          salary = newSalary;
       }
   }
   public double computePay()
   {
      System.out.println("Computing salary pay for " + getName());
      return salary/52;
   }
}

尽管我们不能实例化一个 Employee 类的对象,但是如果我们实例化一个 Salary 类对象,该对象将从 Employee 类继承 7 个成员方法,且通过该方法可以设置或获取三个成员变量。

/* 文件名 : AbstractDemo.java */
public class AbstractDemo
{
   public static void main(String [] args)
   {
      Salary s = new Salary("Mohd Mohtashim", "Ambehta, UP", 3, 3600.00);
      Employee e = new Salary("John Adams", "Boston, MA", 2, 2400.00);

      System.out.println("Call mailCheck using Salary reference --");
      s.mailCheck();

      System.out.println("\n Call mailCheck using Employee reference--");
      e.mailCheck();
    }
}

以上程序编译运行结果如下:

Constructing an Employee
Constructing an Employee
Call mailCheck using  Salary reference --
Within mailCheck of Salary class
Mailing check to Mohd Mohtashim with salary 3600.0

Call mailCheck using Employee reference--
Within mailCheck of Salary class
Mailing check to John Adams with salary 2400.

抽象方法

如果你想设计这样一个类,该类包含一个特别的成员方法,该方法的具体实现由它的子类确定,那么你可以在父类中声明该方法为抽象方法。 Abstract 关键字同样可以用来声明抽象方法,抽象方法只包含一个方法名,而没有方法体。 抽象方法没有定义,方法名后面直接跟一个分号,而不是花括号。

public abstract class Employee
{
   private String name;
   private String address;
   private int number;

   public abstract double computePay();

   //其余代码
}

声明抽象方法会造成以下两个结果: 如果一个类包含抽象方法,那么该类必须是抽象类。 任何子类必须重写父类的抽象方法,或者声明自身为抽象类。 继承抽象方法的子类必须重写该方法。否则,该子类也必须声明为抽象类。最终,必须有子类实现该抽象方法,否则,从最初的父类到最终的子类都不能用来实例化对象。 如果Salary类继承了Employee类,那么它必须实现computePay()方法:

/* 文件名 : Salary.java */
public class Salary extends Employee
{
   private double salary; // Annual salary

   public double computePay()
   {
      System.out.println("Computing salary pay for " + getName());
      return salary/52;
   }

   //其余代码
}

抽象类总结规定

1、 抽象类不能被实例化(初学者很容易犯的错),如果被实例化,就会报错,编译无法通过。只有抽象类的非抽象子类可以创建对象。
2、 抽象类中不一定包含抽象方法,但是有抽象方法的类必定是抽象类。
3、 抽象类中的抽象方法只是声明,不包含方法体,就是不给出方法的具体实现也就是方法的具体功能。
4、 构造方法,类方法(用 static 修饰的方法)不能声明为抽象方法。
5、 抽象类的子类必须给出抽象类中的抽象方法的具体实现,除非该子类也是抽象类

protected

protected修饰的方法和属性对本包和所有子类可见。在实际应用中,要谨慎使用protected属性。

Object

Object类是Java中所有类(不包括基本数据类型)的超类,在Java中每个类都是由它扩展而来的。如果没有明确指出超类,Object就被 认为是这个类的超类,所以不需要这样显式声明:

public Animal extends Object{

}

所有的数组类型,不管是对象数组还是基本类型的数组都扩展Object

Object中的常用方法介绍

equals

Object中的equals方法用来检测一个对象是否等于另一个对象。在Object类中 这个方法将判断两个对象是否具有相同的引用。不过一般情况下,这种方式没有用。比如在 现实中我们通常通过比较id的方式来判断员工是否是同一个人。所以,我们都要重写 Object的equals方法。例如:

public class Employee {

    private String id;

    ...

    @Override
    public boolean equals(Object obj) {
        return id.equals(obj);//String中重写了Object的equals方法,它会比较两个字符串中所有字符是否相等
    }
}

equals方法应该具有的特性:

  • 自反性:对于任何非空引用x,x.equals(x)应该返回true
  • 对称性:对于任何引用x和y,当x.equals(y)返回true时,y.equals(x)也应该返回true
  • 传递性:对于任何引用x,y,z,如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)也应该返回true
  • 一致性:如果x和y的引用没有发生变化,反复调用x.equals(y)应该返回相同的值
  • 对于任何非空引用x,x.equals(null)应该返回false

处理继承问题

使用instanceof,例如:

if(!(other instanceof Animal))return false;

不过考虑一下这样的情况:

A是B的父类,如果B中重写了equals方法,如下

if(!(other instanceof B))return false;

由于:

A instanceof B 返回 false
B instanceof A 返回 true

所以:

B.equals(A);返回 false
A.equals(B);返回 true

不符合对称性。要使符合对称性,就必须让父类的equals方法为final.

使用getClass,不检测继承关系。getClass方法只有当两个引用来自同一类时,才会返回true。例如:

B.getClass() == A.getClass() 返回false

A.getClass() == B.getClass() 返回false

A.getClass() == A.getClass() 返回true

B.getClass() == B.getClass() 返回true

编写equals()方法的建议:

1、 显式参数命名为otherObject
2、 检测thisotherObject是否引用同一个对象。例如:if(this == otherObject)return true;,使用这个就行优化,由于 计算这个等式要比一个一个比较类中的域所付出的代价要小得多。
3、 检测otherObject是否为null.例如if(otherObject == null) return false;
4、 比较thisotherObject是否属于同一个类:
如果equals被子类覆盖,那么应该使用getClass()
如果所有的子类都拥有统一的语义,就使用instanceof.
5、 将otherObject转化为相应类类型的变量ClassName other = (ClassName)otherObject
6、 对所有需要比较的域就行比较。基本类型使用==,对象域使用equals.

hashCode

散列码(hash code)是由对象导出的一个整数值。散列码是没有规律的。如果x和y是两个不同的 对象,x.hashCode()y.hashCode基本不会相同。

hashCode()没被覆写时,默认是返回对象的储存地址。

重写hashCode()方法:

public class Employee {

    private String id;

    private String name;

    ...

    @Override
    public int hashCode() {
        return name.hashCode()+id.hashCode()*5;
    }
}

注意:equalshashCode的定义必须一致:如果x.equals(y)返回true那么x.hashCode()就必须等于y.hashCode()

toString

toString方法用来返回对象值的字符串.

Objects

Objects是一个工具类,它提供了很多实用方法,例如:

int compare(T a, T b, Comparator c)
如果参数相同,则返回0,否则返回c.compare(a,b)。因此,如果两个参数都为null,则返回0。

boolean deepEquals(Object a, Object b)
检查两个对象是否相等。如果两个参数都相等,则返回true。否则,它返回false。如果两个参数都为null,则返回true。

boolean equals(Object a, Object b)
比较两个对象是否相等。如果两个参数相等,则返回true。否则,它返回false。如果两个参数都为null,则返回true。

int hash(Object... values)
为所有指定的对象生成哈希码。它可以用于计算对象的哈希码,该哈希码基于多个实例字段。

int hashCode(Object o)
返回指定对象的哈希码值。如果参数为null,则返回0。

boolean isNull(Object obj)
如果指定的对象为null,isNull()方法返回true。否则,它返回false。您还可以使用比较运算符==检查对象是否为null,例如,obj == null返回obj的true为null。

boolean nonNull(Object obj)
执行与isNull()方法相反的检查。

T requireNonNull(T obj)
T requireNonNull(T obj, String message)
T requireNonNull(T obj, Supplier messageSupplier)
检查参数是否为null。如果参数为null,它会抛出一个NullPointerException异常。此方法设计用于验证方法和构造函数的参数。
第二个版本可以指定当参数为null时抛出的NullPointerException的消息。
第三个版本的方法将一个Supplier作为第二个参数。

String toString(Object o)
String toString(Object o, String nullDefault)
如果参数为null,则toString()方法返回一个“null”字符串。对于非空参数,它返回通过调用参数的toString()方法返回的值。

接口

接口不是类,而是对类的一组需求描述。类描述对象的属性和方法。接口则包含类要实现的方法。 除非实现接口的类是抽象类,否则该类要定义接口中的所有方法。

接口的声明

//接口的声明语法格式如下:[]括起来的表示可有可无

访问修饰符 interface 接口名称 [extends 其他的接口名名] {
        // 声明变量
        // 抽象方法
}

接口的实现

接口的实现要使用Implements关键字,并且要实现接口中所有定义的方法

接口的特点

  • 不能使用new实例化一个接口
  • 接口变量必须引用实现了接口的变量
  • 接口中不能包含成员变量和静态方法(java 8允许),但可以包含常量
  • 接口中所有的方法都自动设置为public,所有的域都自动设置为public static final
  • 接口可以多继承

接口和抽象类的区别

1、 抽象类中的方法可以有方法体,就是能实现方法的具体功能,但是接口中的方法不行。
2、 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的。
3、 接口中不能含有静态代码块以及静态方法(用 static 修饰的方法),而抽象类是可以有静态代码块和静态方法。
4、 一个类只能继承一个抽象类,而一个类却可以实现多个接口。

默认方法

Java 8 新增了接口的默认方法。简单说,默认方法就是接口可以有实现方法,而且不需要实现类去实现其方法。 我们只需在方法名前面加个 default 关键字即可实现默认方法。

为什么要有这个特性?

首先,之前的接口是个双刃剑,好处是面向抽象而不是面向具体编程,缺陷是,当需要修改接口时候,需要修改全部实现该接口的类,目前的 java 8 之前的集合框架没有 foreach 方法,通常能想到的解决办法是在JDK里给相关的接口添加新的方法及实现。然而,对于已经发布的版本,是没法在给接口添加新方法的同时不影响已有的实现。所以引进的默认方法。他们的目的是为了解决接口的修改与现有的实现不兼容的问题。

默认方法语法格式如下:
public interface Vehicle {
   default void print(){
      System.out.println("我是一辆车!");
   }
}

多个默认方法

一个接口有默认方法,考虑这样的情况,一个类实现了多个接口,且这些接口有相同的默认方法,以下实例说明了这种情况的解决方法:

public interface Vehicle {
   default void print(){
      System.out.println("我是一辆车!");
   }
}

public interface FourWheeler {
   default void print(){
      System.out.println("我是一辆四轮车!");
   }
}

第一个解决方案是创建自己的默认方法,来覆盖重写接口的默认方法:

public class Car implements Vehicle, FourWheeler {
   default void print(){
      System.out.println("我是一辆四轮汽车!");
   }
}

第二种解决方案可以使用 super 来调用指定接口的默认方法:

public class Car implements Vehicle, FourWheeler {
   public void print(){
      Vehicle.super.print();
   }
}

静态默认方法

Java 8 的另一个特性是接口可以声明(并且可以提供实现)静态方法。例如:

public interface Vehicle {
   default void print(){
      System.out.println("我是一辆车!");
   }
    // 静态方法
   static void blowHorn(){
      System.out.println("按喇叭!!!");
   }
}

默认方法实例

我们可以通过以下代码来了解关于默认方法的使用,可以将代码放入 Java8Tester.java 文件中:

Java8Tester.java 文件
public class Java8Tester {
   public static void main(String args[]){
      Vehicle vehicle = new Car();
      vehicle.print();
   }
}

interface Vehicle {
   default void print(){
      System.out.println("我是一辆车!");
   }

   static void blowHorn(){
      System.out.println("按喇叭!!!");
   }
}

interface FourWheeler {
   default void print(){
      System.out.println("我是一辆四轮车!");
   }
}

class Car implements Vehicle, FourWheeler {
   public void print(){
      Vehicle.super.print();
      FourWheeler.super.print();
      Vehicle.blowHorn();
      System.out.println("我是一辆汽车!");
   }
}

类优先

情景1

public class People {

    private String name;

    public String getName() {
        System.out.println("people的getName调用");
        return name;
    }
}

public interface Named {
    String getName();
}

public class Test extends People implements Named {//当超类和接口有重名的方法时,不需要子类实现接口方法

}

情景2

public class People {

    private String name;

    public String getName() {
        System.out.println("people的getName调用");
        return name;
    }
}

public interface Named {
   default String getName(){
       System.out.println("默认方法的getName调用");
       return "";
   }
}

public class Test extends People implements Named {//当超类和接口的默认方法重名时,忽略接口的默认方法,即类优先

}

 public static void main(String[] args) {
        Test t = new Test();
        t.getName();//输出为:people的getName调用
    }

注意:千万不能让一个默认方法重新定义Object类中的某个方法,例如,toString。由于类优先原则,这样的 方法绝对无法超越Object.toString.

转自毛先森的博客

Java中实现对象的比较:Comparable接口和Comparator接口

在实际应用中,我们往往有需要比较两个自定义对象大小的地方。而这些自定义对象的比较,就不像简单的整型数据那么简单,它们往往包含有许多的属性,我们一般都是根据这些属性对自定义对象进行比较的。所以Java中要比较对象的大小或者要对对象的集合进行排序,需要通过比较这些对象的某些属性的大小来确定它们之间的大小关系。

一般,Java中通过接口实现两个对象的比较,比较常用就是Comparable接口和Comparator接口。首先类要实现接口,并且使用泛型规定要进行比较的对象所属的类,然后类实现了接口后,还需要实现接口定义的比较方法(compareTo方法或者compare方法),在这些方法中传入需要比较大小的另一个对象,通过选定的成员变量与之比较,如果大于则返回1,小于返回-1,相等返回0。

Comparable接口

什么是Comparable接口

此接口强行对实现它的每个类的对象进行整体排序。此排序被称为该类的自然排序 ,类的 compareTo方法被称为它的自然比较方法 。实现此接口的对象列表(和数组)可以通过 Collections.sort(和 Arrays.sort )进行自动排序。实现此接口的对象可以用作有序映射表中的键或有序集合中的元素,无需指定比较器。

实现什么方法

int compareTo(T o)
比较此对象与指定对象的顺序。如果该对象小于、等于或大于指定对象,则分别返回负整数、零或正整数。

参数: o - 要比较的对象。 
返回:负整数、零或正整数,根据此对象是小于、等于还是大于指定对象。
抛出:ClassCastException - 如果指定对象的类型不允许它与此对象进行比较。

实例

package com.mxl.algorithlm;

import java.util.Date;
/**
 * 因为要实现对ConsumInfo对象的排序,所以在ConsunInfo类中要实现Comparable接口,也就是要实现compareTo()方法
 * 具体的比较参照:依次按照price、uid进行倒序排序
 * @author breeze
 *
 */
public class ConsumInfo implements Comparable<ConsumInfo> {
    private int uid;
    private String name;
    private double price;
    private Date datetime;

    public ConsumInfo() {
        // TODO Auto-generated constructor stub
    }

    public ConsumInfo(int uid,String name,double price,Date datetime){
        this.uid = uid;
        this.name = name;
        this.price = price;
        this.datetime = datetime;

    }

    public int getUid() {
        return uid;
    }

    public void setUid(int uid) {
        this.uid = uid;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }

    public Date getDatetime() {
        return datetime;
    }

    public void setDatetime(Date datetime) {
        this.datetime = datetime;
    }

    @Override
    public String toString() {
        return "ConsumInfo [uid=" + uid + ", name=" + name + ", price=" + price
                + ", datetime=" + datetime + "]";
    }
    /**
     * 这里比较的是什么, Collections.sort方法实现的就是按照此比较的东西排列
     * 顺序(从小到大):
     * if(price < o.price){
            return -1;
        }
        if(price > o.price){
            return 1;
        }
     * 倒序(从大到小):
     * if(price < o.price){
            return 1;
        }
        if(price > o.price){
            return -1;
        }
     * 
     */
    @Override
    public int compareTo(ConsumInfo o) {
        //首先比较price,如果price相同,则比较uid
        if(price < o.price){
            return -1;
        }
        if(price > o.price){
            return 1;
        }

        if(price == o.price){
            if(uid < o.uid){
                return -1;
            }
            if(uid > o.uid){
                return 1;
            }
        }
        return 0;
    }

}

//测试类

package com.mxl.algorithlm;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;

public class ConsumInfoTest {

    public static void main(String[] args) {

        ConsumInfo consumInfo1 = new ConsumInfo(100, "consumInfo1", 400.0,new Date());
        ConsumInfo consumInfo2 = new ConsumInfo(200, "consumInfo1", 200.0,new Date());
        ConsumInfo consumInfo3 = new ConsumInfo(300, "consumInfo1", 100.0,new Date());
        ConsumInfo consumInfo4 = new ConsumInfo(400, "consumInfo1", 700.0,new Date());
        ConsumInfo consumInfo5 = new ConsumInfo(500, "consumInfo1", 800.0,new Date());
        ConsumInfo consumInfo6 = new ConsumInfo(600, "consumInfo1", 300.0,new Date());
        ConsumInfo consumInfo7 = new ConsumInfo(700, "consumInfo1", 900.0,new Date());
        ConsumInfo consumInfo8 = new ConsumInfo(800, "consumInfo1", 400.0,new Date());

        List<ConsumInfo> list = new ArrayList<ConsumInfo>();
        list.add(consumInfo1);
        list.add(consumInfo2);
        list.add(consumInfo3);
        list.add(consumInfo4);
        list.add(consumInfo5);
        list.add(consumInfo6);
        list.add(consumInfo7);
        list.add(consumInfo8);
        System.out.println("排序前:");
        //排序前
        for(ConsumInfo consumInfo : list ){
            System.out.println(consumInfo);
        }

        Collections.sort(list);//排序
        System.out.println("排序后:");
        //排序后
        for(ConsumInfo consumInfo :list){
System.out.println(consumInfo);
        }
    }
}

Comparator接口

与上面的Comparable接口不同的是:

  • Comparator位于包java.util下,而Comparable位于包java.lang下。
  • Comparable接口将比较代码嵌入需要进行比较的类的自身代码中,而Comparator接口在一个独立的类中实现比较。
  • 如果前期类的设计没有考虑到类的Compare问题而没有实现Comparable接口,后期可以通过Comparator接口来实现比较算法进行排序,并且为了使用不同的排序标准做准备,比如:升序、降序。
  • Comparable接口强制进行自然排序,而Comparator接口不强制进行自然排序,可以指定排序顺序。

使用实例:

package test;

import java.util.Comparator;
/**
 * 具体的比较类(比较器),实现Comparator接口
 * @author breeze
 *
 */
public class ComparatorConsunInfo implements Comparator<ConsumInfo> {
    /**
     * 顺序(从小到大):
     * if(price < o.price){
            return -1;
        }
        if(price > o.price){
            return 1;
        }
     * 倒序(从大到小):
     * if(price < o.price){
            return 1;
        }
        if(price > o.price){
            return -1;
        }
     */
    @Override
    public int compare(ConsumInfo o1, ConsumInfo o2) {
         //首先比较price,如果price相同,则比较uid
        if(o1.getPrice() > o2.getPrice()){
            return 1;
        }

        if(o1.getPrice() < o2.getPrice()){
            return -1;
        }

        if(o1.getPrice() == o2.getPrice()){
            if(o1.getUid() > o2.getUid()){
                return 1;
            }
            if(o1.getUid() < o2.getUid()){
                return -1;
            }
        }
        return 0;
    }

}

/**
 * 需要进行比较的类
 * @author breeze
 *
 */
public class ConsumInfo{
    private int uid;
    private String name;
    private double price;
    private Date datetime;

    public ConsumInfo() {
        // TODO Auto-generated constructor stub
    }

    public ConsumInfo(int uid,String name,double price,Date datetime){
        this.uid = uid;
        this.name = name;
        this.price = price;
        this.datetime = datetime;

    }

    public int getUid() {
        return uid;
    }

    public void setUid(int uid) {
        this.uid = uid;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }

    public Date getDatetime() {
        return datetime;
    }

    public void setDatetime(Date datetime) {
        this.datetime = datetime;
    }

    @Override
    public String toString() {
        return "ConsumInfo [uid=" + uid + ", name=" + name + ", price=" + price
                + ", datetime=" + datetime + "]";
    }

}

//测试类
public class ConsumInfoTest {

    public static void main(String[] args) {

        ConsumInfo consumInfo1 = new ConsumInfo(100, "consumInfo1", 400.0,new Date());
        ConsumInfo consumInfo2 = new ConsumInfo(200, "consumInfo1", 200.0,new Date());
        ConsumInfo consumInfo3 = new ConsumInfo(300, "consumInfo1", 100.0,new Date());
        ConsumInfo consumInfo4 = new ConsumInfo(400, "consumInfo1", 700.0,new Date());
        ConsumInfo consumInfo5 = new ConsumInfo(500, "consumInfo1", 800.0,new Date());
        ConsumInfo consumInfo6 = new ConsumInfo(600, "consumInfo1", 300.0,new Date());
        ConsumInfo consumInfo7 = new ConsumInfo(700, "consumInfo1", 900.0,new Date());
        ConsumInfo consumInfo8 = new ConsumInfo(800, "consumInfo1", 400.0,new Date());

        List<ConsumInfo> list = new ArrayList<ConsumInfo>();
        list.add(consumInfo1);
        list.add(consumInfo2);
        list.add(consumInfo3);
        list.add(consumInfo4);
        list.add(consumInfo5);
        list.add(consumInfo6);
        list.add(consumInfo7);
        list.add(consumInfo8);

        System.out.println("排序前:");
        //排序前
        for(ConsumInfo consumInfo : list ){
            System.out.println(consumInfo);
        }
        ComparatorConsunInfo comparatorConsunInfo = new ComparatorConsunInfo();//比较器
        Collections.sort(list,comparatorConsunInfo);//排序
        System.out.println("排序后:");
        //排序后
        for(ConsumInfo consumInfo :list){
            System.out.println(consumInfo);
        }
    }
}

lambda表达式

函数式接口是指接口中只有一个抽象方法(Java8中接口中可以声明非抽象方法),例如Comparator:

@FunctionalInterface
public interface Comparator<T> {

    int compare(T o1, T o2);

    //重新声明Object中的方法会让方法不再是抽象方法
    boolean equals(Object obj);

    default Comparator<T> reversed() {
        return Collections.reverseOrder(this);
    }

    public static <T> Comparator<T> nullsFirst(Comparator<? super T> comparator) {
        return new Comparators.NullComparator<>(true, comparator);
    }

    ....

}

使用实例

//注意:由于带Comparator的sort方法参数声明为泛型,所有无法使用基本类型
Integer word[] =   new Integer[]{12,23};
Arrays.sort(word,(e1, e2)-> e1 - e2);

//上面的代码相当于下面的代码,可以看出lambda可以使代码简洁
Integer word[] =   new Integer[]{12,23};
Arrays.sort(word, new Comparator<Integer>() {
          @Override
          public int compare(Integer e1, Integer e2) {
              return e1 - e2;
          }
      });

Predicate<Boolean> isNull = Objects::isNull;
System.out.println(isNull.test(null));
//等同于
Predicate<Boolean> isNull = new Predicate<Boolean>() {
            @Override
            public boolean test(Boolean aBoolean) {
                return Objects.isNull(aBoolean);
            }
        };
    System.out.println(isNull.test(null));

lambda表达式的方法引用

方法引用的三种情况

  • object::instanceMethod(对象方法)
  • Class::staticMethod (静态方法)
  • Class::instanceMethod (对象方法)

前两种情况中,方法引用等价于提供方法参数的lambda,例如Math::pow等价于(x,y)->Math.pow(x,y)

第三种情况,第一个参数会成为方法的目标,例如String::compareToIgnoreCase等价于(x,y)->x.compareToIgnore(y)

lambda表达式的构造器引用

例如A::new是A的构造器引用,至于使用哪一个构造器,取决于上下文。

lambda表达式变量的作用域

 public void requestMessage(String text,int delay){
        ActionListener actionListener = e -> {
            System.out.println(text);//使用变量text
            ...
        };
    }

可以把一个lambda表达式转化为包含一个方法的对象,这样自由变量的值就会复制到这个对象的实例变量中去

注意:lambda表达式中捕获的变量必须是最终值;而且在lambda中声明于一个局部变量同名的参数或局部变量是不合法的;在lambda表达式中使用this关键字时,是指创建这个lambda表达式的方法的this参数

常用的函数式接口

函数式接口 参数类型 返回类型 抽象方法名 描述
Runnable void run 作为无参数或返回值的动作运行
Predicate<T> T boolean test 布尔值函数
BiPredicate<T,U> T,U boolean test 有两个参数的boolean函数
Supplier<T> T get 提供一个T型的值
Consumer<T> T void accept 处理一个T类型的值
BiConsumer<T,U> T,U void accept 处理T和U类型的值
Function<T,R> T R apply 由T转化为R类型
BiFunction<T,U,R> T,U R apply 由T,U类型转化为R类型
UnaryOperator<T> T T applay UnaryOperator继承Function,由T类型返回T类型
BinaryOperator<T> T,T T applay BinaryOperator继承BiFunction

基本类型的函数式接口

为了减少自动装箱,一个尽量使用基本类型的函数式接口,如图:

61_10.png

Java内部类

转载海子博客

  说起内部类这个词,想必很多人都不陌生,但是又会觉得不熟悉。原因是平时编写代码时可能用到的场景不多,用得最多的是在有事件监听的情况下, 并且即使用到也很少去总结内部类的用法。今天我们就来一探究竟。下面是本文的目录大纲:

一. 内部类基础
二. 深入理解内部类
三. 内部类的使用场景和好处
四. 常见的与内部类相关的笔试面试题

若有不正之处,请多谅解并欢迎批评指正。
请尊重作者劳动成果,转载请标明原文链接:
tech.souyunku.com/dolphin0520…

内部类基础

  在Java中,可以将一个类定义在另一个类里面或者一个方法里面,这样的类称为内部类。广泛意义上的内部类一般来说包括这四种:成员内部类、局部内部类、匿名内部类和静态内部类。下面就先来了解一下这四种内部类的用法。

成员内部类

成员内部类是最普通的内部类,它的定义为位于另一个类的内部,形如下面的形式:

class Circle {
    double radius = 0;

    public Circle(double radius) {
        this.radius = radius;
    }

    class Draw {     //内部类
        public void drawSahpe() {
            System.out.println("drawshape");
        }
    }
}

这样看起来,类Draw像是类Circle的一个成员,Circle称为外部类。成员内部类可以无条件访问外部类的所有成员属性和成员方法(包括private成员和静态成员)。

class Circle {
    private double radius = 0;
    public static int count =1;
    public Circle(double radius) {
        this.radius = radius;
    }

    class Draw {     //内部类
        public void drawSahpe() {
            System.out.println(radius);  //外部类的private成员
            System.out.println(count);   //外部类的静态成员
        }
    }
}

不过要注意的是,当成员内部类拥有和外部类同名的成员变量或者方法时,会发生隐藏现象,即默认情况下访问的是成员内部类的成员。如果要访问外部类的同名成员,需要以下面的形式进行访问:

外部类.this.成员变量
外部类.this.成员方法

  虽然成员内部类可以无条件地访问外部类的成员,而外部类想访问成员内部类的成员却不是这么随心所欲了。在外部类中如果要访问成员内部类的成员,必须先创建一个成员内部类的对象,再通过指向这个对象的引用来访问:

class Circle {
    private double radius = 0;

    public Circle(double radius) {
        this.radius = radius;
        getDrawInstance().drawSahpe();   //必须先创建成员内部类的对象,再进行访问
    }

    private Draw getDrawInstance() {
        return new Draw();
    }

    class Draw {     //内部类
        public void drawSahpe() {
            System.out.println(radius);  //外部类的private成员
        }
    }
}

成员内部类是依附外部类而存在的,也就是说,如果要创建成员内部类的对象,前提是必须存在一个外部类的对象。创建成员内部类对象的一般方式如下:

public class Test {
    public static void main(String[] args)  {
        //第一种方式:
        Outter outter = new Outter();
        Outter.Inner inner = outter.new Inner();  //必须通过Outter对象来创建

        //第二种方式:
        Outter.Inner inner1 = outter.getInnerInstance();
    }
}

class Outter {
    private Inner inner = null;
    public Outter() {

    }

    public Inner getInnerInstance() {
        if(inner == null)
            inner = new Inner();
        return inner;
    }

    class Inner {
        public Inner() {

        }
    }
}

内部类可以拥有private访问权限、protected访问权限、public访问权限及包访问权限。比如上面的例子,如果成员内部类Inner用private修饰,则只能在外部类的内部访问,如果用public修饰,则任何地方都能访问;如果用protected修饰,则只能在同一个包下或者继承外部类的情况下访问;如果是默认访问权限,则只能在同一个包下访问。这一点和外部类有一点不一样,外部类只能被public和包访问两种权限修饰。我个人是这么理解的,由于成员内部类看起来像是外部类的一个成员,所以可以像类的成员一样拥有多种权限修饰。

局部内部类

  局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内。

class People{
    public People() {

    }
}

class Man{
    public Man(){

    }

    public People getWoman(){
        class Woman extends People{   //局部内部类
            int age =0;
        }
        return new Woman();
    }
}

注意,局部内部类就像是方法里面的一个局部变量一样,是不能有public、protected、private以及static修饰符的。

匿名内部类

匿名内部类应该是平时我们编写代码时用得最多的,在编写事件监听的代码时使用匿名内部类不但方便,而且使代码更加容易维护。下面这段代码是一段Android事件监听代码:

scan_bt.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                // TODO Auto-generated method stub

            }
        });

        history_bt.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                // TODO Auto-generated method stub

            }
        });

这段代码为两个按钮设置监听器,这里面就使用了匿名内部类。这段代码中的:

new OnClickListener() {

            @Override
            public void onClick(View v) {
                // TODO Auto-generated method stub

            }
        }

就是匿名内部类的使用。代码中需要给按钮设置监听器对象,使用匿名内部类能够在实现父类或者接口中的方法情况下同时产生一个相应的对象,但是前提是这个父类或者接口必须先存在才能这样使用。当然像下面这种写法也是可以的,跟上面使用匿名内部类达到效果相同。

private void setListener()
{
    scan_bt.setOnClickListener(new Listener1());        
    history_bt.setOnClickListener(new Listener2());
}

class Listener1 implements View.OnClickListener{
    @Override
    public void onClick(View v) {
    // TODO Auto-generated method stub

    }
}

class Listener2 implements View.OnClickListener{
    @Override
    public void onClick(View v) {
    // TODO Auto-generated method stub

    }
}

这种写法虽然能达到一样的效果,但是既冗长又难以维护,所以一般使用匿名内部类的方法来编写事件监听代码。同样的,匿名内部类也是不能有访问修饰符和static修饰符的。

  匿名内部类是唯一一种没有构造器的类。正因为其没有构造器,所以匿名内部类的使用范围非常有限,大部分匿名内部类用于接口回调。匿名内部类在编译的时候由系统自动起名为Outter$1.class。一般来说,匿名内部类用于继承其他类或是实现接口,并不需要增加额外的方法,只是对继承方法的实现或是重写。

静态内部类

静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字static。静态内部类是不需要依赖于外部类的,这点和类的静态成员属性有点类似,并且它不能使用外部类的非static成员变量或者方法,这点很好理解,因为在没有外部类的对象的情况下,可以创建静态内部类的对象,如果允许访问外部类的非static成员就会产生矛盾,因为外部类的非static成员必须依附于具体的对象。

public class Test {
    public static void main(String[] args)  {
        Outter.Inner inner = new Outter.Inner();
    }
}

class Outter {
    public Outter() {

    }

    static class Inner {
        public Inner() {

        }
    }
}

深入理解内部类

1、为什么成员内部类可以无条件访问外部类的成员?

  在此之前,我们已经讨论过了成员内部类可以无条件访问外部类的成员,那具体究竟是如何实现的呢?下面通过反编译字节码文件看看究竟。事实上,编译器在进行编译的时候,会将成员内部类单独编译成一个字节码文件,下面是Outter.java的代码:

public class Outter {
    private Inner inner = null;
    public Outter() {

    }

    public Inner getInnerInstance() {
        if(inner == null)
            inner = new Inner();
        return inner;
    }

    protected class Inner {
        public Inner() {

        }
    }
}

编译之后,出现了两个字节码文件:

61_11.png

反编译Outter$Inner.class文件得到下面信息:

E:\Workspace\Test\bin\com\cxh\test2>javap -v Outter$Inner
Compiled from "Outter.java"
public class com.cxh.test2.Outter$Inner extends java.lang.Object
  SourceFile: "Outter.java"
  InnerClass:
   #24= #1 of #22; //Inner=class com/cxh/test2/Outter$Inner of class com/cxh/tes
t2/Outter
  minor version: 0
  major version: 50
  Constant pool:
const #1 = class        #2;     //  com/cxh/test2/Outter$Inner
const #2 = Asciz        com/cxh/test2/Outter$Inner;
const #3 = class        #4;     //  java/lang/Object
const #4 = Asciz        java/lang/Object;
const #5 = Asciz        this$0;
const #6 = Asciz        Lcom/cxh/test2/Outter;;
const #7 = Asciz        <init>;
const #8 = Asciz        (Lcom/cxh/test2/Outter;)V;
const #9 = Asciz        Code;
const #10 = Field       #1.#11; //  com/cxh/test2/Outter$Inner.this$0:Lcom/cxh/t
est2/Outter;
const #11 = NameAndType #5:#6;//  this$0:Lcom/cxh/test2/Outter;
const #12 = Method      #3.#13; //  java/lang/Object."<init>":()V
const #13 = NameAndType #7:#14;//  "<init>":()V
const #14 = Asciz       ()V;
const #15 = Asciz       LineNumberTable;
const #16 = Asciz       LocalVariableTable;
const #17 = Asciz       this;
const #18 = Asciz       Lcom/cxh/test2/Outter$Inner;;
const #19 = Asciz       SourceFile;
const #20 = Asciz       Outter.java;
const #21 = Asciz       InnerClasses;
const #22 = class       #23;    //  com/cxh/test2/Outter
const #23 = Asciz       com/cxh/test2/Outter;
const #24 = Asciz       Inner;

{
final com.cxh.test2.Outter this$0;

public com.cxh.test2.Outter$Inner(com.cxh.test2.Outter);
  Code:
   Stack=2, Locals=2, Args_size=2
   0:   aload_0
   1:   aload_1
   2:   putfield        #10; //Field this$0:Lcom/cxh/test2/Outter;
   5:   aload_0
   6:   invokespecial   #12; //Method java/lang/Object."<init>":()V
   9:   return
  LineNumberTable:
   line 16: 0
   line 18: 9

  LocalVariableTable:
   Start  Length  Slot  Name   Signature
   0      10      0    this       Lcom/cxh/test2/Outter$Inner;
}

第11行到35行是常量池的内容,下面逐一第38行的内容: final com.cxh.test2.Outter this$0;

  这行是一个指向外部类对象的指针,看到这里想必大家豁然开朗了。也就是说编译器会默认为成员内部类添加了一个指向外部类对象的引用,那么这个引用是如何赋初值的呢?下面接着看内部类的构造器:

public com.cxh.test2.Outter$Inner(com.cxh.test2.Outter);

  从这里可以看出,虽然我们在定义的内部类的构造器是无参构造器,编译器还是会默认添加一个参数,该参数的类型为指向外部类对象的一个引用,所以成员内部类中的Outter this&0 指针便指向了外部类对象,因此可以在成员内部类中随意访问外部类的成员。从这里也间接说明了成员内部类是依赖于外部类的,如果没有创建外部类的对象,则无法对Outter this&0引用进行初始化赋值,也就无法创建成员内部类的对象了。

2、为什么局部内部类和匿名内部类只能访问局部final变量?

想必这个问题也曾经困扰过很多人,在讨论这个问题之前,先看下面这段代码:

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

    }

    public void test(final int b) {
        final int a = 10;
        new Thread(){
            public void run() {
                System.out.println(a);
                System.out.println(b);
            };
        }.start();
    }
}

这段代码会被编译成两个class文件:Test.class和Test

1、class。默认情况下,编译器会为匿名内部类和局部内部类起名为Outter x.class(x为正整数)。

61_12.png

     根据上图可知,test方法中的匿名内部类的名字被起为 Test$1。

  上段代码中,如果把变量a和b前面的任一个final去掉,这段代码都编译不过。我们先考虑这样一个问题:

  当test方法执行完毕之后,变量a的生命周期就结束了,而此时Thread对象的生命周期很可能还没有结束,那么在Thread的run方法中继续访问变量a就变成不可能了,但是又要实现这样的效果,怎么办呢?Java采用了 复制 的手段来解决这个问题。将这段代码的字节码反编译可以得到下面的内容:

61_13.png

  我们看到在run方法中有一条指令: bipush 10

  这条指令表示将操作数10压栈,表示使用的是一个本地局部变量。这个过程是在编译期间由编译器默认进行,如果这个变量的值在编译期间可以确定,则编译器默认会在匿名内部类(局部内部类)的常量池中添加一个内容相等的字面量或直接将相应的字节码嵌入到执行字节码中。这样一来,匿名内部类使用的变量是另一个局部变量,只不过值和方法中局部变量的值相等,因此和方法中的局部变量完全独立开。

  下面再看一个例子:

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

    }

    public void test(final int a) {
        new Thread(){
            public void run() {
                System.out.println(a);
            };
        }.start();
    }
}

  反编译得到:

61_14.png

  我们看到匿名内部类Test$1的构造器含有两个参数,一个是指向外部类对象的引用,一个是int型变量,很显然,这里是将变量test方法中的形参a以参数的形式传进来对匿名内部类中的拷贝(变量a的拷贝)进行赋值初始化。

  也就说如果局部变量的值在编译期间就可以确定,则直接在匿名内部里面创建一个拷贝。如果局部变量的值无法在编译期间确定,则通过构造器传参的方式来对拷贝进行初始化赋值。

  从上面可以看出,在run方法中访问的变量a根本就不是test方法中的局部变量a。这样一来就解决了前面所说的 生命周期不一致的问题。但是新的问题又来了,既然在run方法中访问的变量a和test方法中的变量a不是同一个变量,当在run方法中改变变量a的值的话,会出现什么情况?

  对,会造成数据不一致性,这样就达不到原本的意图和要求。为了解决这个问题,java编译器就限定必须将变量a限制为final变量,不允许对变量a进行更改(对于引用类型的变量,是不允许指向新的对象),这样数据不一致性的问题就得以解决了。

  到这里,想必大家应该清楚为何 方法中的局部变量和形参都必须用final进行限定了。

3、静态内部类有特殊的地方吗?

  从前面可以知道,静态内部类是不依赖于外部类的,也就说可以在不创建外部类对象的情况下创建内部类的对象。另外,静态内部类是不持有指向外部类对象的引用的,这个读者可以自己尝试反编译class文件看一下就知道了,是没有Outter this&0引用的。

内部类的使用场景和好处

  为什么在Java中需要内部类?总结一下主要有以下四点:

1、 每个内部类都能独立的继承一个接口的实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响。内部类使得多继承的解决方案变得完整,
2、 方便将存在一定逻辑关系的类组织在一起,又可以对外界隐藏。
3、 方便编写事件驱动程序
4、 方便编写线程代码

  个人觉得第一点是最重要的原因之一,内部类的存在使得Java的多继承机制变得更加完善。

四.常见的与内部类相关的笔试面试题

1、根据注释填写(1),(2),(3)处的代码

public class Test{
    public static void main(String[] args){
           // 初始化Bean1
           (1)
           bean1.I++;
           // 初始化Bean2
           (2)
           bean2.J++;
           //初始化Bean3
           (3)
           bean3.k++;
    }
    class Bean1{
           public int I = 0;
    }

    static class Bean2{
           public int J = 0;
    }
}

class Bean{
    class Bean3{
           public int k = 0;
    }
}

  从前面可知,对于成员内部类,必须先产生外部类的实例化对象,才能产生内部类的实例化对象。而静态内部类不用产生外部类的实例化对象即可产生内部类的实例化对象。

  创建静态内部类对象的一般形式为: 外部类类名.内部类类名 xxx = new 外部类类名.内部类类名()   创建成员内部类对象的一般形式为: 外部类类名.内部类类名 xxx = 外部类对象名.new 内部类类名()   因此,(1),(2),(3)处的代码分别为:

Test test = new Test();    

  Test.Bean1 bean1 = test.new Bean1();   

Test.Bean2 b2 = new Test.Bean2();    

Bean bean = new Bean();     

Bean.Bean3 bean3 =  bean.new Bean3();

2、下面这段代码的输出结果是什么?

public class Test {
    public static void main(String[] args)  {
        Outter outter = new Outter();
        outter.new Inner().print();
    }
}

class Outter
{
    private int a = 1;
    class Inner {
        private int a = 2;
        public void print() {
            int a = 3;
            System.out.println("局部变量:" + a);
            System.out.println("内部类变量:" + this.a);
            System.out.println("外部类变量:" + Outter.this.a);
        }
    }
}

  最后补充一点知识:关于成员内部类的继承问题。一般来说,内部类是很少用来作为继承用的。但是当用来继承的话,要注意两点:

1、 成员内部类的引用方式必须为 Outter.Inner.
2、 构造器中必须有指向外部类对象的引用,并通过这个引用调用super()。这段代码摘自《Java编程思想》

class WithInner {
    class Inner{

    }
}
class InheritInner extends WithInner.Inner { 

    // InheritInner() 是不能通过编译的,一定要加上形参 
    InheritInner(WithInner wi) { 
        wi.super(); //必须有这句调用
    } 

    public static void main(String[] args) { 
        WithInner wi = new WithInner(); 
        InheritInner obj = new InheritInner(wi); 
    } 
} 

参考资料:

文章永久链接:https://tech.souyunku.com/28204

未经允许不得转载:搜云库技术团队 » 《Java核心技术》第10版读书总结(一)

JetBrains 全家桶,激活、破解、教程

提供 JetBrains 全家桶激活码、注册码、破解补丁下载及详细激活教程,支持 IntelliJ IDEA、PyCharm、WebStorm 等工具的永久激活。无论是破解教程,还是最新激活码,均可免费获得,帮助开发者解决常见激活问题,确保轻松破解并快速使用 JetBrains 软件。获取免费的破解补丁和激活码,快速解决激活难题,全面覆盖 2024/2025 版本!

联系我们联系我们