类和对象
类和对象
概念:类是自定义的数据类型,对象就是一个具体的实例 <==> int 和 100 的关系
对象【属性,行为】
包
概念:包本质就是文件夹,用来存放java文件
作用: 1.区分相同名字的类 2.可以很好的管理类 3.控制方法范围通过访问修饰符
// 声明当前类所在的包,需要放在类的最上面 ,一个类最多只有一句package
package com.al_tair.study;
命名规则和规范
包的命名规则:只能包含数字,字母,下划线,小圆点,但不能用数字开头,不能是关键字或者保留字
包的命名规范:全名都是小写字母(包名和包名之间用点来分隔)
比如 : com.公司名.项目名.业务模块名
系统的包导入
// 声明当前类所在的包
package com.al_tair.study;
// 导入包,import关键字放在package下面,在类定义前面,可以有多句,没有顺序要求
import java.util.Scanner;
import java.util.Arrays;
// 类定义
public class study{
public static void main(String[]args){}
}
.* 指的是包下所有的类都导入(注意 java.util.* 导入的是util下的类,而不会导入util.Stream.*中Stream下的类)
修饰符
访问修饰符
- public : 对全部包中的类开放
- protected : 对不同包的子类和同一个包中的类公开
- 默认访问修饰符(无修饰符):对同一个包中的类(包含子类)公开,但是对不同包的子类不公开
- private : 只有类本身可以访问
使用注意细节
- 修饰符可以用来修饰类中的属性,成员方法以及类
- 局部变量不能有访问修饰符,因为范围已经被局限在方法或者代码块等等
- 只能用默认的和public来修饰类
- protected 用于修饰成员,表示在继承体系中成员对于子类可见,但是这个访问修饰符对于类没有意义
static静态修饰符
类的组成:成员变量,成员方法,代码块,内部类,构造器
被static静态修饰符修饰:静态成员变量,静态成员方法,静态代码块,静态内部类,构造器不能被static修饰
static静态修饰符表示的更多是共享数据,是属于类的,随着类的加载而加载,随着类的消亡而销毁(具体介绍如下文类的组成)
final修饰符
final 关键字可以用来修饰类,属性,方法和局部变量,不能修饰构造器
public class Final {
/*
* 使用final的情况
* (1)当不希望类被继承时,可以用final修饰
* (2)当不希望父类的某个方法被子类覆盖或者重写
* (3)当不希望类的某个属性的值被修改,可以用final修饰 [例如:final double PI = 3.1415926] -> 常量
* (4)当不希望某个局部变量被修改,可以用final修饰 -> 局部常量
*
* 使用final关键字的细节讨论
* 1.final修饰的属性又叫常量,一般用 XXX_XXX_XXX来命名
* 2.final修饰的属性在定义时,必须赋初始值,赋值可以加在如下的位置上
* 属性定义时初始化 / 在构造器中 / 在代码块中
* 3.final修饰的属性是静态的,则赋值可以加在如下的位置上
* 静态属性定义时初始化 / 在静态代码块中
* 4.final类不能继承,但是可以实例化对象
* 5.如果类不是final类,但是含有final方法,虽然该方法不能重写,但是该类可以被继承
* 6.final 和 static 往往搭配使用效率更高,不会导致类加载,底层编译器做了优化 如下例子
* 7.包装类(Integer,double,Float,Boolean等)不能继承
*/
public static void main(String[] args) {
/*
* final 修饰基本数据类型
* 基本数据类型的值不能发生变化
*/
final int FINAL = 30;
// FINAL = 100; 报错
/*
* final修饰引用数据类型
* 引用数据类型饿地址值不能发生改变,但是该地址上的内容可以发生改变
*/
final FU fu = new FU();
// fu.show4();
// fu = new FU(); 报错
// final 和 static 往往搭配使用效率更高,不会导致类加载,底层编译器做了优化
System.out.println(BB.name);
}
}
class AA{
// 静态属性定义时初始化
public static final int NUMS1 = 12;
public final int NUM1 = 12;
public static final int NUMS2;
public final int NUM2;
public static final int NUMS3;
public final int NUM3;
// 在静态代码块中初始化
static{
NUMS2 = 12;
NUMS3 = 12;
}
{
NUM3 = 12;
}
// 在构造器中初始化
public AA(){
NUM2 = 12;
}
}
// final 和 static 往往搭配使用效率更高,不会导致类加载,底层编译器做了优化
class BB{
public static final String name = "yyds";
static {
System.out.println("类加载了");
}
// 运行效果:不会显示类加载了 说明类没有被加载
}
类的组成
成员变量
普通成员变量
格式:<public|protected|默认|private> [static] [final] <type> <variable_name>
// <public|protected|默认|private> 访问修饰符
// [static] [final] 可以选择添加,具体作用接下来的会有专门讲解
// <type> 数据类型
// <variable_name> 变量名
实例
// 成员变量 = 属性 = 字段(field)
class Person{
// 属性的数据类型可以是基本数据类型或者引用数据类型
String name;
int height;
int weight;
String[] friends;
}
成员变量如果不赋值,有默认初始值
数据类型 | byte | short | char | int | long | float | double | boolean | String |
---|---|---|---|---|---|---|---|---|---|
初始值 | 0 | 0 | \u0000 | 0 | 0 | 0.0 | 0.0 | false | null |
静态成员变量
静态变量也称作类变量
语法:访问修饰符 static 数据类型 变量名; (访问修饰符和static的顺序可以交换)
与普通成员变量的语法区别就是有无 static 关键字
注意细节
- 静态变量是同一个类所有对象共享
- 类变量的生命周期随着类的加载开始,随着类消亡而销毁
- 类变量中不能使用和对象有关的关键字(比如:this,super ),因为this或者super的产生是伴随着对象的创建而出现的,但是类变量在类加载的时候就出现了
访问方式: 类名.类变量(推荐) 对象名.类变量名
// 举例类变量和类方法的使用
// Math类的源码
public static final double PI = 3.14159265358979323846;
// 取绝对值
@HotSpotIntrinsicCandidate
public static int abs(int a) {
return (a < 0) ? -a : a;
}
成员方法
普通成员方法
方法本质就是操作数据
// 格式:伪代码
访问修饰符 返回的数据类型 方法名(形参列表..){
方法体: 实现某种功能
return 返回的数据类型: // 返回类型可以是任何数据类型(数组或对象等等)
}
实例
// 注意:方法不能嵌套使用
public static void main(String[]args){
Person p new Person();
// "XXX"是实参,实参:调用该方法时传入的参数
p.run("XXX");
}
class Person{
// name为形参,形参:在形参列表上定义的参数
void run(String name){
String name = name; // 局部变量,作用范围就在这个方法
System.out.println(name+" "+"runing..");
}
}
// 区分实参和形参:实参是调用该方法时传入的参数,形参是在形参列表上定义的参数,需要满足:个数相同,数据类型相同或者可以自动转换,顺序对应
// 1.基本数据类型传递的是值,形参的任何改变不影响实参
// 2.引用数据类型传递的是地址,比如:数组可以通过形参来修改实参引用的数据参数,但是无法修改引用数据类型的引用地址值
局部变量
主要的变量就是成员变量和局部变量(除成员变量外就是局部变量)
格式:数据类型 变量名 = 数值
// 局部变量可以是代码块,方法,构造器等等,作用都一样,我在此以方法的局部变量进行详细说明
class person{
// 属性(成员变量,全局变量)作用范围在至少在整个类中,具体看访问修饰符
// 可以不用赋值,初始值等价于数组
String name;
void say(){
// 局部变量 作用范围在该方法中
// 注意:局部变量不可以添加修饰符;没有赋值则无法使用
String content = "lns";
System.out.println(content);
}
}
成员变量和局部变量的区别
-
成员变量可以和局部变量重名,访问时遵循就近原则
class scope{ String name = "罗念笙"; void show(){ System.out.println("方法中:"+name); // 罗念笙 } { String name = "张洛融"; System.out.println("代码块:"+name); // 张洛融 } }
-
在同一作用域中,成员变量和局部变量都不能重复出现
- 成员变量作用范围在当前类以及创建的对象
- 局部变量的作用范围在当前代码块,方法, 循环等等
-
生命周期的区别,成员变量的生命周期由创建对象和销毁对象决定;局部变量的生命周期由创建代码块或者方法和销毁代码块或者方法决定
-
修饰符不同:成员变量可以有修饰符,但是局部变量不可以添加修饰符
递归调用
概念:方法调用它自己本身
// 例子:阶乘
// 注意:递归必须会有结束语句,否则就会无限循环,最终导致栈溢出
public int factorial(int num){
if(num == 1) return num; // 结束语句
else return num * factorial(num-1);
}
反转链表(206)
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
示例 1:输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
示例 2:输入:head = [1,2]
输出:[2,1]
示例 3:输入:head = []
输出:[]
递归算法
我们很多时候对于递归算法都有种排斥,那就是无从入手,原本正向推导就是件不容易的事,现在还要先反向构思,这难度可想而知,对初学者特别不友好。
不妨听我说说递归算法解决思路:
我觉得在使用递归算法之前,你先问问自己三个问题
- 判断递归结束的条件是什么?(并且该结束条件最终是一定会执行到的,不然就会造成死循环)
- 该递归方法的作用是什么? 换句话来说你想通过这个递归方法得到什么?
- 得到该递归方法之后,我们还需要什么才可以达到题目的要求?
/**
* 代码实现
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {
if(head == null) return null;
else if(head.next == null){
return head;
}else{
ListNode list = reverseList(head.next);
ListNode Node = list;
while(Node.next != null){
Node = Node.next;
}
Node.next = head;
Node.next.next = null;
return list;
}
}
}
分析过程
判断递归结束的条件:
// 这个条件也是一定会执行到的
else if(head.next == null){
return head;
}
该递归方法的作用:
本题的作用就是获得head反转链表(就是题目要求的实现)
// reverseList(head.next)这个作用就是获得head.next的反转链表,但是不要问为什么,因为你在这一步得不到答案,你只要假定这个方法就是这个作用就可以了
ListNode list = reverseList(head.next);
得到该递归方法之后,我们还需要什么才可以达到题目的要求:
head.next 以后的链表是反转的,那我们只要在该节点的最后添加上head的节点是不是整体就实现了反转
然后我们反思一下是不是每次调用递归方法后我们都是干同一件事,那就是把第一节点移动到最后,这不就实现了链表最后的反转嘛(寻找到这个共同特征,那么递归方法就迎刃而解了)
// 遍历 head.next反转后的链表,并在最后接上当前的head的节点
ListNode Node = list;
while(Node.next != null){
Node = Node.next;
}
Node.next = head;
Node.next.next = null;
return list;
递归习题练习(注重规律 + 条件)
// 1.斐波那契数 1 1 2 3 5 8 ... 后面的数是前面两个数之和(n > 2)
// 规律:要求的那个数 = 要求的那个数前面的数 + 要求的那个数前面的前面的数 (条件是大于2)
public int Fibonacci(int n){
if(n == 2 || n == 1){
return 1;
}
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
// 2.猴子吃桃 原题: 一天少一半并再多次一个,当第十天发现只剩下一个桃子,试问最初有几个桃子
// 规律:要求的当天桃子数 = (要求的明天的桃子 + 1) / 2 (条件是桃子数不等于1)
public int peachMonkey(){
if(peachMonkey() == 1){
return 1;
}
return peachMonkey()/2-1;
}
/**
* 3.迷宫 8*7 (1 障碍物 0 可以通过的路)
* 1 1 1 1 1 1 1
* 1 0 0 0 0 0 1
* 1 0 0 0 0 0 1
* 1 1 1 0 0 0 1
* 1 0 0 0 0 0 1
* 1 0 0 0 0 0 1
* 1 0 0 0 0 0 1
* 1 1 1 1 1 1 1
*
* 起点坐标(2,-2) ==> 终点坐标(6,-7)
* 转换成数组坐标 起点坐标(1,1) ==> 终点坐标(6,5)
*/
class miGong{
public static void main(String[] args) {
// 创建迷宫 8*7
int[][] map = new int[8][7];
for (int i = 0; i < map.length; i++) {
map[i][0] = 1;
map[i][map[0].length-1] = 1;
}
for (int i = 0; i < map[0].length; i++) {
map[0][i] = 1;
map[map.length-1][i] = 1;
}
map[3][1] = 1;
map[3][2] = 1;
for (int i = 0; i < map.length; i++) {
for (int j = 0; j < map[0].length;j++) {
System.out.print(map[i][j]+" ");
}
System.out.println();
}
System.out.println("===================");
miGong miGong = new miGong();
if(miGong.findWay(map,1,1)){
for (int i = 0; i < map.length; i++) {
for (int j = 0; j < map[0].length;j++) {
System.out.print(map[i][j]+" ");
}
System.out.println();
}
}else{
System.out.println("迷宫没有出入");
}
}
/**
* 递归方法解决走出迷宫问题
* line , col 指的是当前的位置
* 该方法判断当前的路能否可以走
*/
public Boolean findWay(int[][]map,int line,int col){
// map 数组的各个值的含义 0 可以走的路但是没有走过 1 障碍物 2 表示走过之后可以走的路 3 表示走过,走不通的路
if(map[6][5] == 2){
return true;
}else{
// 没走过
if(map[line][col] == 0){
map[line][col] = 2;
// 尝试向四个方向探索
if(findWay(map,line - 1,col)){ // 上
return true;
}else if(findWay(map,line + 1,col)){ // 下
return true;
}else if(findWay(map,line,col - 1)){ // 左
return true;
}else if(findWay(map,line,col + 1)){ // 右
return true;
}else{
map[line][col] = 3;
return false;
}
}else{
return false;
}
}
}
}
// 运行效果
1 1 1 1 1 1 1
1 0 0 0 0 0 1
1 0 0 0 0 0 1
1 1 1 0 0 0 1
1 0 0 0 0 0 1
1 0 0 0 0 0 1
1 0 0 0 0 0 1
1 1 1 1 1 1 1
===================
1 1 1 1 1 1 1
1 2 2 2 2 2 1
1 2 2 2 2 2 1
1 1 1 2 2 2 1
1 3 3 2 2 2 1
1 3 3 2 2 2 1
1 3 3 2 2 2 1
1 1 1 1 1 1 1
// 4.汉诺塔
class tower{
public static void main(String[] args) {
tower.move(64,'a','b','c');
}
// 方法:移动汉诺塔
public static void move(int num,char a,char b,char c){
// 如果只有一个盘
if(num == 1){
System.out.println(a +"->"+ c);
}else{
// 将当前的盘分成两部分,分别是:最底下一个盘以及上面的盘
// 1.将上面的盘移到b位置借助c位置,然后将a位置最底下的盘移动到c位置
move(num - 1,a,c,b);
System.out.println(a +"->"+ c);
// 2.将b位置上的盘分成两部分,分别是:最底下一个盘以及上面的盘
// 可以将以下代码优化成: move(num - 1,b,a,c);
if(num == 2){
// 2.1 如果b位置上只有一个盘,那就直接将b位置上的盘移动到c位置上
System.out.println(b +"->" + c);
}else{
// 2.2 反之则将b位置上的上面的盘移动到a位置上,然后将最底下的盘移动到c位置上
move(num - 2,b,c,a);
System.out.println(b +"->"+ c);
move(num - 2,a,b,c);
}
}
}
}
// 运行效果
a->c
a->b
c->b
a->c
b->a
b->c
a->c
方法重载
条件:方法名必须相同 ; 形参列表类型,类型顺序或者数量不一致 ; 与返回类型无关 ; 方法的修饰符可以不同
如果编译器找不到匹配的参数,就会产生编译时错误,因为不存在该匹配,查询匹配的过程称重载解析
比如:输出println语句
方法重写
方法重写又称方法覆盖,可以简单理解为就是子类覆盖了父类的某些方法(比如 String 的 equals())
可以通过@Override来进行检查是否真正的重写
条件
- 重写方法不能缩小访问权限(public > protected > 默认 > private)
- 如果子类的方法重写了父类的方法,那么子类中该方法的访问级别不允许低于父类的访问级别。这是为了确保可以使用父类实例的地方都可以使用子类实例,也就是确保满足里氏替换原则
- 参数列表必须与被重写方法相同
- 当子类和父类有一个方法的名称以及参数相同时就构成了 方法的重写,返回类型必须是相同的或者构成父类返回父类的类型 <> 子类返回子类的类型,否则会报错!
- 重写方法不能抛出新的异常,或者超过了父类范围的异常,但是可以抛出更少、更有限的异常,或者不抛出异常
方法重载和和重写的区别
同样的一个方法能够根据输入数据的不同,做出不同的处理,即方法的重载——有不同的参数列表(静态多态性)
而当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的操作时,你就要覆盖父类方法,即在子类中重写该方法——相同参数,不同实现(动态多态性)
名称 | 发生范围 | 方法名 | 形参列表 | 返回类型 | 修饰符 |
---|---|---|---|---|---|
重载(overload) | 本类 | 必须一样 | 类型,个数或者顺序至少有一个不同 | 无要求 | 无要求 |
重写(override) | 父子类 | 必须一样 | 必须相同 | 返回的类型和父类返回的类型相同,或者是其子类 | 子类方法的访问范围大于等于父类方法的访问范围 |
可变参数(了解)
概念:将同一个类中多个同名同功能同数据类型但参数不同的方法,封装成一个方法,就可以通过可变参数实现 (本质上就是数组形参)
格式:访问修饰符 返回数据类型 方法名(数据类型... 形参名)
// 求和公式
class Method{
// 整数型
public int add(int... nums){
int sum = 0;
for (int i = 0; i < nums.length; i++) {
sum += nums[i];
}
return sum;
}
// 浮点型
public double add(double... nums){
double sum = 0;
for (int i = 0; i < nums.length; i++) {
sum += nums[i];
}
return sum;
}
}
注意细节
-
可变参数传入的实参可以为0或任意多个
-
可变参数传入的实参可以为数组(实际上就是二维数组)
-
可变参数可以和普通数据类型的参数放在参数列表中,但是必须保证可变参数放在最后
// 一个参数列表,可变参数的个数只能出现一个 public double add(int num,double... nums){} // right public double add(int... num,double... nums){} // error
静态方法
静态方法也称作类方法
与成员方法的语法区别就是有无 static 关键字
注意细节
- 类方法量是同一个类所有对象共享
- 类方法中不能使用和对象有关的关键字(比如:,this,super ),因为this或者super的产生需要创建对象,但是类方法在类加载的时候就出现了
- 类方法只能访问类变量或者类方法,但是反之普通成员方法既可以访问非静态成员,也可以访问静态成员
- 类方法的生命周期随着类的加载开始,随着类消亡而销毁
- 如果父类中含有一个静态方法,且在子类中也含有一个返回类型、方法名、参数列表均与之相同的静态方法,那么该子类实际上只是将父类中的该同名方法进行了隐藏,而非重写。换句话说,父类和子类中含有的其实是两个没有关系的方法,它们的行为也并不具有多态性
Main方法
Main方法就是我们最常用的类方法(静态方法)
public class HelloMain {
/*
* 1.main方法时虚拟机调用
* 2.java虚拟机调用类的main方法,所以该方法的访问权限必须是public
* 3.java虚拟机在执行main()方法时不必创建对象,所以用static
* 4.Java执行程序 参数1 参数2 参数3【dos界面操作说明: java HelloMain hello main !】 参数1:hello 参数2:main 参数3:!
*
* 特别提示:
* 1.在main()方法中,我们可以直接调用main方法所在的类的静态方法和静态属性
* 2.但是不能直接访问该类中的非静态成员,必须创建该类的一个实例对象后,才能通过这个对象访问非静态成员
*/
public static void main(String[]args){
// args 是如何传入,在idea上设置 Run -> Edit Configurations -> program arguments:
// 遍历显示
for(int i=0;i<args.length;i++){
System.out.println("第"+(i+1)+"个参数"+args[i]);
}
}
}
代码块
普通代码块
普通代码块又称初始化块,属于类中的成员部分,没有方法名,没有返回,没有形参,只有方法体;普通代码块不需要被调用,而是加载对象的时候隐式调用
语法:{ // 方法体 };( 结尾分号可有可无 )
public class CodeBlock {
/*
* 代码块
* 1.相当于另外一种形式的构造器(对构造器的补充机制),可以做初始化的操作
* 2.场景:如果多个构造器中都有重复的语句,可以抽取到初始化块中,提高代码的重用性(如下)
*/
public static void main(String[]args){
new Movie("爱情神话");
}
}
class Movie{
private String name;
private double price;
private String director;
/*
* 下面的三个构造函数都有相同的语句,这样看起来重复
* 代码块的使用就可以很好的解决这个问题
*/
{
System.out.println("电影开始~~");
};
public Movie(){}
public Movie(String name){
// System.out.println("电影开始~~");
this.name = name;
}
public Movie(String name,double price){
// System.out.println("电影开始~~");
this.name = name;
this.price = price;
}
public Movie(String name,double price,String director){
// System.out.println("电影开始~~");
this.name = name;
this.price = price;
this.director = director;
}
}
注意细节
- 普通代码块是随着对象的创建而执行的,每次创建对象都会调用代码块
- 如果不创建对象,只是使用类的静态成员,不会执行普通代码块
静态代码块
静态代码块,属于类中的成员部分,没有方法名,没有返回,没有形参,只有方法体和修饰符static;静态代码块不需要被调用,而是加载类的时候隐式调用
语法:static{ // 方法体 };( 结尾分号可有可无 )
类什么时候被加载?
- 创建子类对象化实例或者使用子类的静态成员时,父类也会被加载
- 使用类的静态成员时(静态属性,静态方法)
父类和子类的静态代码块和普通代码块执行顺序是什么呢?
- 在父类和子类都有静态代码块和普通代码块时候,创建子类实例时候的执行顺序: 父类静态代码块 --> 子类静态代码块 --> 父类普通代码块 --> 子类普通代码块
- 在父类和子类都有静态代码块和普通代码块时候,创建父类实例时候的执行顺序: 父类静态代码块 --> 父类普通代码块
- 在父类和子类都有静态代码块和普通代码块时候,使用父类的静态成员时只调用父类的静态代码块
- 在父类和子类都有静态代码块和普通代码块时候,使用子类的静态成员时候的执行顺序: 父类静态代码块 --> 子类静态代码块
代码示例
public class StaticCodeBlock {
/*
* 静态属性初始化,静态代码块,静态方法,构造器的优先级
* 1.父类的静态属性初始化优先级 == 父类的静态代码块优先级 (看代码的执行顺序)
* 2.子类的静态属性初始化优先级 == 子类的静态代码块优先级 (看代码的执行顺序)
* 3.父类的成员变量初始化优先级 == 父类的普通代码块优先级 (看代码的执行顺序)
* 4.父类的构造器
* 5.子类的成员变量初始化优先级 == 子类的普通代码块优先级 (看代码的执行顺序)
* 6.子类的构造器
* 方法调用的时候才会执行
* public 构造函数名(){
* 1)super()
* 2)普通代码块
* 3)构造函数内容
* }
*/
public static void main(String[]args){
// 使用类的静态成员时(静态属性,静态方法)
// AA.name = "念笙";
// AA.show();
// 创建对象实例时(new)
// new AA();
// 创建子类的对象实例,父类也会被加载
// new BB();
// 使用子类的静态成员时(静态属性,静态方法)
// BB.sex = "男";
// BB.show();
// 静态初始化,静态代码块,静态方法的优先级
new CC();
// CC.show();
}
}
class AA{
public static String name;
static {
System.out.println("AA静态代码块被调用");
}
{
System.out.println("AA普通代码块被调用");
}
public static void show(){
System.out.println("name:"+name);
}
}
class BB extends AA{
public static String sex;
static {
System.out.println("BB静态代码块被调用");
}
{
System.out.println("BB普通代码块被调用");
}
public static void show(){
System.out.println("sex:"+sex);
}
}
class CC{
public CC(){
// 1)super()
// 2)普通代码块
System.out.println("构造器被调用");
}
// 静态属性初始化
private static int age = getAge();
// 静态方法
public static void show(){
System.out.println("age:"+age);
}
// 静态代码块
static{
System.out.println("CC静态代码块被调用");
}
public static int getAge(){
System.out.println("age:"+age);
return 18;
}
}
内部类
概念:一个类的内部嵌套了另一个类结构,被嵌套的类称为内部类,嵌套其他类的类称为外部类(注意类的五大成员:属性,方法,构造器,代码块,内部类)
语法:class Outer{ // 外部类 class Inner{ // 内部类 } }
class OuterClass{ // 外部类
int n1 = 100; // 属性
public OuterClass(int n3){} // 构造方法
void n2(){} //方法
{} // 代码块
class InnerClass{} // 内部类
}
内部类的分类:
- 定义在外部类局部位置(比如方法内)
- 局部内部类(有类名)
- 匿名内部类(没有类名,重点)
- 定义在外部类的成员位置上
- 成员内部类(没有static修饰)
- 静态内部类(有static修饰)
局部内部类
概念:局部内部类是定义在外部类的局部位置(比如:方法或者代码块),有类名
- 局部内部类可以访问外部类的所有成员变量,包含私有的类的成员
- 局部内部类不能添加访问修饰符,但是是可以被final修饰(不被继承)
- 作用域:仅在定义它的方法或代码块中
- 外部类在方法中可以创建InnerPartClass对象,然后调用局部内部类的方法,不允许在方法外创建该对象
- 如果外部类和局部内部类的成员重名时,默认遵循就近原则,如果就想访问外部类的成员,则可以使用(外部类名.this.成员)
class OuterClass02{ // 外部类
private int n1 = 100;
public void n2(){
class InnerPartClass{
private int n1 = 20;
public void n3(){
// 如果外部类和局部内部类的成员重名时,默认遵循就近原则,如果想访问外部类的成员,则可以使用(外部类名.this.成员)
System.out.println(n1); // 20
// 解读OuterClass02.this 本质就是外部类的对象即那个对象调用了n2方法,OuterClass02.this就是那个对象,这里是out对象调用的,因此 OuterClass02.this指向out对象(动态绑定)
System.out.println(OuterClass02.this.n1); // 100
System.out.println(OuterClass02.this); // com.Al_tair.innerClass_.OuterClass02@27d6c5e0
}
}
// 外部类在方法中可以创建InnerPartClass对象,然后调用局部内部类的方法
InnerPartClass innerPartClass = new InnerPartClass();
innerPartClass.n3();
}
}
public class InnerClass01 { // 外部其他类
public static void main(String[] args) {
OuterClass02 out = new OuterClass02();
out.n2();
System.out.println(out); // com.Al_tair.innerClass_.OuterClass02@27d6c5e0
}
}
匿名内部类
概念:定义在外部类的局部位置,没有类名
作用域: 仅仅在定义它的方法和代码块中
语法:new 类名或接口名(参数列表){ // 匿名内部类 }
class OutClass{
private int n = 10;
public void meathod(){
/*
* 使用匿名内部类简化开发
* aa的编译类型 -- AA ; aa的运行类型 -- 匿名内部类OutClass$1 (外部类名$匿名内部类的序号)
* 1.创建匿名内部类后马上创建该实例,并返回该地址给aa
* 2.匿名内部类只能使用一次,但是该实例对象可以反复引用,就是不能用一个匿名内部类创建多个对象实例
* 3.不能添加访问修饰符,因为它的就是一个局部变量
* 4.如果外部类和局部内部类的成员重名时,默认遵循就近原则,如果想访问外部类的成员,则可以使用(外部类名.this.成员)
*/
// 第一种调用匿名内部类的内部方法
// 这里new的不是接口实例化,而是接口的匿名内部实现类
AA aa = new AA(){ // 向上转型
@Override
public void cry() {
System.out.println("嘤嘤嘤~~");
}
};
aa.cry();
System.out.println(aa.getClass()); // class com.Al_tair.innerClass_.OutClass$1
// 第二种调用匿名内部类的内部方法
System.out.println(
new AA(){
@Override
public void cry() {
System.out.println("555~~");
}
}.getClass()
); // class com.Al_tair.innerClass_.OutClass$2
}
}
interface AA{
void cry();
}
// 调用者
public class AnonymousInnerClass {
public static void main(String[] args) {
OutClass outClass = new OutClass();
outClass.meathod();
}
}
对比传统方式和匿名内部类的区别
- 如果实现该接口或者类的对象只被使用少次,建议使用匿名内部类
- 反之需要反复调用就可以使用传统方式减少代码的复用
// 代码示例
public class Test {
public static void main(String[] args) {
// 当作实参直接传递,简洁高效
show(new Paint() {
@Override
public void show() {
System.out.println("最美油画!");
}
});
// 传统方式
new Picture().show();
}
public static void show(Paint paint){
paint.show();
}
}
// Paint接口
interface Paint{
public void show();
}
class Picture implements Paint{
@Override
public void show() {
System.out.println("最美油画!");
}
}
成员内部类
概念:成员内部类是定义在外部类的成员位置,并且没有static修饰
作用域: 可以直接访问外部类的所有成员
public class MemberInnerDemo01 {
public static void main(String[] args) {
// 成员内部类的访问方式
// 1.创建外部类的对象来调用(推荐)
// 1.1.创建外部类的对象
MemberOuterClass memberOuterClass = new MemberOuterClass();
// 1.2.创建成员内部类的对象 需要加外部类名 (如: 外部类名.内部类名 对象引用名 = 外部类的对象名.new 内部类名();)
MemberOuterClass.InnerClass innerClass = memberOuterClass.new InnerClass();
// 2.调用外部类方法访问成员内部类
MemberOuterClass.InnerClass innerClass2 = new MemberOuterClass().getInnerInstance();
}
}
class MemberOuterClass{
/*
* 成员内部类(没有static修饰)
* 1.可以添加任意的访问修饰符: public 默认 protected private
* 2.成员内部类的访问方式
* 1.成员内部类 —> 外部类成员 [直接访问]
* 2.外部类 -> 成员内部类 [创建成员内部类的对象,再访问]
* 3.其他外部类 -> 成员内部类 [创建外部类的对象,创建成员内部类的对象,再由访问修饰符决定访问权]
* 3.如果外部类和局部内部类的成员重名时,默认遵循就近原则,如果想访问外部类的成员,则可以使用(外部类名.this.成员)
*/
protected class InnerClass{}
public InnerClass getInnerInstance(){
return new InnerClass();
}
}
静态内部类
概念:成员内部类是定义在外部类的成员位置,并且有static修饰
作用域: 可以直接访问内部类的所有静态成员,包含私有的,但是不能访问非静态成员
class StaticOuterClass{
static class StaticInner{
public void say(){
System.out.println("StaticInner 在 saying");
}
}
}
public class StaticInnerClass {
public static void main(String[] args) {
/*
* 静态内部类 static修饰
* 1.可以添加任何的访问修饰符: public 默认 protected private
* 2.静态内部类的访问方式(2和3创建静态内部类的对象写法不一样)
* 1.静态内部类 -> 外部类 [直接访问所有静态成员]
* 2.外部类 -> 静态内部类 [创建静态内部类的对象,再访问]
* 3.其他外部类 -> 静态内部类 [创建静态内部类的对象,再由访问修饰符决定访问权]
* 3.如果外部类和局部内部类的成员重名时,默认遵循就近原则,如果想访问外部类的成员,则可以使用(外部类名.成员)
*/
StaticOuterClass.StaticInner staticClass = new StaticOuterClass.StaticInner();
staticClass.say();
}
}
构造器
构造器的概念
构造器是一个特殊的方法,没有返回值,同时是用来类创建对象的纽带
格式: 修饰符 方法名(形参列表){ 方法体 }
class person{
int age;
String name;
// 构造器也称构造方法
// 1.构造器修饰符:与普通方法相同
// 2.构造器没有返回值
// 3.方法名必须和类名一样!!!
// 4.在创建对象时,系统会自动的调用该类的构造器完成对对象的初始化(注意:构造器非创建对象)
public person(String name,int age){
this.age = age;
this.name = name;
}
// 5.如果没有有参构造器,则有一个默认的无参构造器,如果有有参构造器,则没有默认的无参构造器
// 6.构造器重载,一个类可以有多个构造器
public person(){}
}
创建对象
// 先声明后创建
Car car;
// 调用无参构造器
car = new Car(); // car 是对象引用,非对象本身,new Car()是对象本身
// 直接创建
Car car = new Car();
类和对象的内存图
public static void main(String[] args){
Cat cat = new Car(); // 无参构造器
cat.name = "小黄";
cat.age = 18;
cat.color = "黄色";
}
class Cat{
String name;
int age;
String color;
}
this关键字
概念:jvm虚拟机会给每个对象分配this,代表当前对象
class person{
int age;
String name;
// this可以看作该类的属性,value就是该类的地址,我们可以通过对象的hashcode() == this.hashcode()来判断哪个对象调用,这个this就是指的是哪个对象
public person(String name,int age){
this.age = age;
this.name = name;
}
}
注意细节
-
this关键字可以用来区分成员变量和局部变量
-
this关键字可以用来访问本类中的属性,成员方法,构造器
// this不能在类定义的外部使用,只能在类定义的方法或者构造器中使用 class Method{ String content = "成功!"; // 访问构造器语句:this(参数列表) // 注意:this调用构造器的时候必须方法第一条语句 public Method(){ this("优秀!") System.out.println("无参构造方法"); } void f1(){ // 访问属性 String con = this.content; System.out.println("f1方法"); } // 访问成员方法的语法:this.方法名(参数列表); void f2(){ this.f1(); System.out.println("f2方法"); }
super关键字
用处:super 代表父类的引用,用于访问父类的方法,属性和构造器
注意细节:
- 无法访问父类的私有方法和属性
- 使用的时候必须放在构造器的第一行,因此只能调用一次父类的构造器
- 当子类的属性,方法和父类重名时,为了访问父类的成员,必须通过super关键字来完成
this 和 super 的区别
区别点 | this | super |
---|---|---|
访问属性 | 访问本类中的属性,如果本类没有此属性则从父类中继续查找 | 从父类开始查找属性 |
调用方法 | 访问本类中的方法,如果本类没有此方法则从父类中继续查找 | 从父类开始查找方法 |
调用构造器 | 调用本类的构造器,必须放在构造器的首行 | 调用父类的构造器,必须放在子类构造器的首行 |
特殊 | 表示当前对象 | 子类中访问父类对象 |
面向对象编程的三大特征
封装
概念:就是把抽象的数据【属性】和对数据的操作【方法】封装在一起,数据被保护在内部,程序的其他部分只能通过被授权的操作【方法】,才能对数据进行操作
好处: 1.隐藏实现的细节 2.可以对数据进行验证,保证数据安全合理
封装的实现步骤
-
将属性进行私有化,不能直接修改属性
-
提供一个公共的set方法,用于对属性判断,赋值
public void setXxx(类型 参数名){ // Xxx表示某个属性的set方法 // 加入数据验证的业务逻辑 // this.属性=参数名 }
-
提供一个公共的get方法,用于获取属性的值
public 数据类型 getXxx(){ // 权限判断,Xxx某个属性 return Xxx; }
继承
格式:class 子类 extends 父类 { } (extends 关键字)
好处:解决代码复用性,当多个类中有相同的属性和方法时候,我们可以抽出相同属性和方法作为父类
注意细节
-
子类继承了父类所有的属性和方法,但是私有属性和方法不能被直接访问,子类和父类之间必须满足 is - a 的逻辑关系
-
子类必须调用父类的构造器,完成父类的初始化,默认无参父类构造器,当父类没有无参构造器并且子类未用super指明对应的父类的构造器,编译不会通过
// 父类 public class person { public person() { System.out.println("父类构造器"); } } // 子类 public class Child extends person{ public Child() { // 默认 super(); System.out.println("子类构造器"); } } class test{ public static void main(String[] args) { Child child = new Child(); } } // 运行结果 父类构造器 子类构造器
-
super and this 关键字使用的时候必须放在构造器的第一行,因此这两个方法调用不能共存在一个构造器
-
java所有类都是Object类的子类
-
父类的构造器的调用不仅限于直接父类,会一直追朔直到Object类(顶级父类)
-
子类最多只能继承一个父类(单继承机制)
内存分布图
查找属性和方法数据根据就近原则(从调用的类的属性和方法开始,依次向父类寻找,只要找到该属性或者方法则不再继续寻找)
多态
概念:方法或者对象具有多种状态,多态是建立在封装和继承之上的
方法的重载和重写体现了方法的多态
对象的多态具体体现
-
前提条件:两个对象(类)存在继承关系
-
一个对象的编译类型和运行类型可以不一致
// Animal 父类 Dog 子类 Cat 子类 Animal animal = new Dog(); // 编译类型 Aniaml 运行类型 Dog animal = new Cat(); // 运行类型是可以改变的
-
编译类型在定义对象时候就确定了
-
运行类型是可以改变的
-
编译类型看定义,运行类型看 new 对象
细节分析
-
多态的向上转型
- 本质:父类的引用指向子类的对象
- 语法:父类类型 引用名 = new 子类类型();
- 特点:编译类型看定义,运行类型看 new 对象
- 用该对象调用方法的时候,只能调用父类中的方法,不能调用子类特有的方法,因为编译不通过;但是调用方法在运行类型中是先从子类开始查找方法
- 属性没有重写的说法,所以属性的值看编译类型
-
多态的向下转型
-
语法:子类类型 引用名 = (子类类型)父类引用;
-
只能强转父类的引用,不能强转父类的对象
-
可以调用子类类型中的所有成员
-
向下转型是为了方便能访问子类独有的属性或者方法
// 举例 Animal animal = new Dog(); // 编译类型 Aniaml 运行类型 Dog Dog dog = (Dog) animal; // // 编译类型 Dog 运行类型 Dog
-
动态绑定机制
具体体现:
- 当调用对象方法时候,该方法会和该对象的内存地址/ 运行类型进行动态绑定
- 当调用对象的属性的时候没有动态绑定的说法
举例说明:
// 举例
public class demo{
public static void main(String[] args){
father f = new son();
System.out.println(f.sum()); // 40
System.out.println(f.sum2()); // 60
}
}
class father{ // 父类
public int attribute = 10;
public int sum(){
return getAttribute() + 10; // getAttribute()绑定的是子类的该方法
}
public int sum2(){
return attribute + 50; // 属性没有动态绑定机制
}
public int getAttribute(){
return attribute;
}
}
class son extends father{ // 子类
public int attribute = 30;
public int getAttribute(){
return attribute;
}
}
五大设计原则
1. 单一责任原则
分治
换句话说就是让一个类只负责一件事,当这个类需要做过多事情的时候,就需要分解这个类
如果一个类承担的职责过多,就等于把这些职责耦合在了一起,一个职责的变化可能会削弱这个类完成其它职责的能力
2. 开放封闭原则
类应该对扩展开放,对修改关闭
扩展就是添加新功能的意思,因此该原则要求在添加新功能时不需要修改代码
符合开闭原则最典型的设计模式是装饰者模式,它可以动态地将责任附加到对象上,而不用去修改类的代码
3. 里氏替换原则
子类对象必须能够替换掉所有父类对象
继承是一种 IS-A 关系,子类需要能够当成父类来使用,并且需要比父类更特殊
如果不满足这个原则,那么各个子类的行为上就会有很大差异,增加继承体系的复杂度
4. 接口分离原则
不应该强迫客户依赖于它们不用的方法
因此使用多个专门的接口比使用单一的总接口要好
5. 依赖倒置原则
高层模块不应该依赖于低层模块,二者都应该依赖于抽象;
抽象不应该依赖于细节,细节应该依赖于抽象
高层模块包含一个应用程序中重要的策略选择和业务模块,如果高层模块依赖于低层模块,那么低层模块的改动就会直接影响到高层模块,从而迫使高层模块也需要改动
依赖于抽象意味着:
- 任何变量都不应该持有一个指向具体类的指针或者引用;
- 任何类都不应该从具体类派生;
- 任何方法都不应该覆写它的任何基类中的已经实现的方法
其他常见原则
除了上述的经典原则,在实际开发中还有下面这些常见的设计原则。
1. 迪米特法则
迪米特法则又叫作最少知识原则(Least Knowledge Principle,简写 LKP),就是说一个对象应当对其他对象有尽可能少的了解,不和陌生人说话
2. 合成复用原则
尽量使用对象组合,而不是通过继承来达到复用的目的
3. 共同封闭原则
一起修改的类,应该组合在一起(同一个包里)。如果必须修改应用程序里的代码,我们希望所有的修改都发生在一个包里(修改关闭),而不是遍布在很多包里
4. 稳定抽象原则
最稳定的包应该是最抽象的包,不稳定的包应该是具体的包,即包的抽象程度跟它的稳定性成正比
5. 稳定依赖原则
包之间的依赖关系都应该是稳定方向依赖的,包要依赖的包要比自己更具有稳定性