持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1 天,点击查看活动详情
Java对象和多态 (面向对象)
面向对象基础
面向对象程序设计(Object Oriented Programming)
对象基于类创建,类相当于一个模板,对象就是根据模板创建出来的实体(就像做月饼,我们要做一个月饼首先需要一个模具,模具就是我们的类,而做出来的月饼,就是类的实现,也叫做对象),类是抽象的数据类型,并不能代表某一个具体的事物,类是对象的一个模板。类具有自己的属性,包括成员变量、成员方法等,我们可以调用类的成员方法来让类进行一些操作。
Scanner sc = new Scanner(System.in);
String str = sc.nextLine();
System.out.println("你输入了:"+str);
sc.close();
所有的对象,都需要通过new关键字创建,基本数据类型不是对象!Java不是纯面对对象语言!
不是基本类型的变量,都是引用类型,引用类型变量代表一个对象,而基本数据类型变量,保存的是基本数据类型的值,我们可以通过引用来对对象进行操作。(最好不要理解为引用指向对象的地址,初学者不要谈内存,学到JVM时再来讨论)
对象占用的内存由JVM统一管理,不需要手动释放内存,当一个对象不再使用时(比如失去引用或是离开了作用域)会被JVM自动清理,内存管理更方便!
类的基本结构
为了快速掌握,我们自己创建一个自己的类,创建的类文件名称应该和类名一致。
成员变量
在类中,可以包含许多的成员变量,也叫成员属性,成员字段(field)通过.来访问我们类中的成员变量,我们可以通过类创建的对象来访问和修改这些变量。成员变量是属于对象的!
public class Test {
int age;
String name;
}
public static void main(String[] args) {
Test test = new Test();
test.name = "奥利给";
System.out.println(test.name);
}
成员变量默认带有初始值,也可以自己定义初始值。
成员方法
我们之前的学习中接触过方法(Method)吗?主方法!
public static void main(String[] args) {
//Body
}
方法是语句的集合,是为了完成某件事情而存在的。完成某件事情,可以有结果,也可以做了就做了,不返回结果。比如计算两个数字的和,我们需要得到计算后的结果,所以说方法需要有返回值;又比如,我们只想吧数字打印在控制台,只需要打印就行,不用给我结果,所以说方法不需要有返回值。
方法的定义和使用
在类中,我们可以定义自己的方法,格式如下:
[返回值类型] 方法名称([参数]){
//方法体
return 结果;
}
- 返回值类型:可以是引用类型和基本类型,还可以是void,表示没有返回值
- 方法名称:和标识符的规则一致,和变量一样,规范小写字母开头!
- 参数:例如方法需要计算两个数的和,那么我们就要把两个数到底是什么告诉方法,那么它们就可以作为参数传入方法
- 方法体:方法具体要干的事情
- 结果:方法执行的结果通过return返回(如果返回类型为void,可以省略return)
非void方法中,return关键字不一定需要放在最后,但是一定要保证方法在任何情况下都具有返回值!
int test(int a){
if(a > 0){
//缺少retrun语句!
}else{
return 0;
}
}
return也能用来提前结束整个方法,无论此时程序执行到何处,无论return位于哪里,都会立即结束个方法!
void main(String[] args) {
for (int i = 0; i < 10; i++) {
if(i == 1) return; //在循环内返回了!和break区别?
}
System.out.println("淦"); //还会到这里吗?
}
传入方法的参数,如果是基本类型,会在调用方法的时候,对参数的值进行复制,方法中的参数变量,不是我们传入的变量本身!
public static void main(String[] args) {
int a = 10, b = 20;
new Test().swap(a, b);
System.out.println("a="+a+", b="+b);
}
public class Test{
void swap(int a, int b){ //传递的仅仅是值而已!
int temp = a;
a = b;
b = temp;
}
}
传入方法的参数,如果是引用类型,那么传入的依然是该对象的引用!(类似于C语言的指针)
public class B{
String name;
}
public class A{
void test(B b){ //传递的是对象的引用,而不是值
System.out.println(b.name);
}
}
public static void main(String[] args) {
int a = 10, b = 20;
B b = new B();
b.name = "lbw";
new A().test(b);
System.out.println("a="+a+", b="+b);
}
方法之间可以相互调用
void a(){
//xxxx
}
void b(){
a();
}
当方法在自己内部调用自己时,称为递归调用(递归很危险,慎重!)
int a(){
return a();
}
成员方法和成员变量一样,是属于对象的,只能通过对象去调用!
对象设计练习
- 学生应该具有以下属性:名字、年龄
- 学生应该具有以下行为:学习、运动、说话
方法的重载
一个类中可以包含多个同名的方法,但是需要的形式参数不一样。(补充:形式参数就是定义方法需要的参数,实际参数就传入的参数)方法的返回类型,可以相同,也可以不同,但是仅返回类型不同,是不允许的!
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;
}
}
现在我们有很多种重写的方法,那么传入实参后,到底进了哪个方法呢?
public class Test {
void a(int i){
System.out.println("调用了int");
}
void a(short i){
System.out.println("调用了short");
}
void a(long i){
System.out.println("调用了long");
}
void a(char i){
System.out.println("调用了char");
}
void a(double i){
System.out.println("调用了double");
}
void a(float i){
System.out.println("调用了float");
}
public static void main(String[] args) {
Test test = new Test();
test.a(1); //直接输入整数
test.a(1.0); //直接输入小数
short s = 2;
test.a(s); //会对号入座吗?
test.a(1.0F);
}
}
构造方法
构造方法(构造器)没有返回值,也可以理解为,返回的是当前对象的引用!每一个类都默认自带一个无参构造方法。
//反编译结果
package com.test;
public class Test {
public Test() { //即使你什么都不编写,也自带一个无参构造方法,只是默认是隐藏的
}
}
反编译其实就是把我们编译好的class文件变回Java源代码。
Test test = new Test(); //实际上存在Test()这个的方法,new关键字就是用来创建并得到引用的
// new + 你想要使用的构造方法
这种方法没有写明返回值,但是每个类都必须具有这个方法!只有调用类的构造方法,才能创建类的对象!
类要在一开始准备的所有东西,都会在构造方法里面执行,完成构造方法的内容后,才能创建出对象!
一般最常用的就是给成员属性赋初始值:
public class Student {
String name;
Student(){
name = "伞兵一号";
}
}
我们可以手动指定有参构造,当遇到名称冲突时,需要用到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();
}
}
静态变量和静态方法
静态变量和静态方法是类具有的属性(后面还会提到静态类、静态代码块),也可以理解为是所有对象共享的内容。我们通过使用static关键字来声明一个变量或一个方法为静态的,一旦被声明为静态,那么通过这个类创建的所有对象,操作的都是同一个目标,也就是说,对象再多,也只有这一个静态的变量或方法。那么,一个对象改变了静态变量的值,那么其他的对象读取的就是被改变的值。
public class Student {
static int a;
}
public static void main(String[] args) {
Student s1 = new Student();
s1.a = 10;
Student s2 = new Student();
System.out.println(s2.a);
}
不推荐使用对象来调用,被标记为静态的内容,可以直接通过类名.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);
}
}
定义和赋值是两个阶段,在定义时会使用默认值(上面讲的,类的成员变量会有默认值)定义出来之后,如果发现有赋值语句,再进行赋值,而这时,调用了静态方法,所以说会先去加载静态方法,静态方法调用时拿到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类
字符串类是一个比较特殊的类,他是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 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.*
静态导入
静态导入可以直接导入某个类的静态方法或者是静态变量,导入后,相当于这个方法或是类在定义在当前类中,可以直接调用该方法。
import static com.test.ui.Student.test;
public class Main {
public static void main(String[] args) {
test();
}
}
静态导入不会进行类的初始化!
访问控制
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
}
数组类型
假设出现一种情况,我想记录100个数字,定义100个变量还可行吗?
我们可以使用到数组,数组是相同类型数据的有序集合。数组可以代表任何相同类型的一组内容(包括引用类型和基本类型)其中存放的每一个数据称为数组的一个元素,数组的下标是从0开始,也就是第一个元素的索引是0!
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语言创建的数组需要手动设置默认值),我们可以通过下标去访问:
int[] arr = new int[10];
arr[0] = 626;
System.out.println(arr[0]);
System.out.println(arr[1]);
我们可以通过数组变量名称.length来获取当前数组长度:
int[] arr = new int[]{1, 2, 3};
System.out.println(arr.length); //打印length成员变量的值
数组在创建时,就固定长度,不可更改!访问超出数组长度的内容,会出现错误!
String[] arr = new String[10];
System.out.println(arr[10]); //出现异常!
//Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 11
// at com.test.Application.main(Application.java:7)
思考:能不能直接修改length的值来实现动态扩容呢?
int[] arr = new int[]{1, 2, 3};
arr.length = 10;
数组做实参,因为数组也是类,所以形参得到的是数组的引用而不是复制的数组,操作的依然是数组对象本身
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;
}
数组的遍历
如果我们想要快速打印数组中的每一个元素,又怎么办呢?
传统for循环
我们很容易就联想到for循环
int[] arr = new int[]{1, 2, 3};
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
foreach
传统for循环虽然可控性高,但是不够省事,要写一大堆东西,有没有一种省事的写法呢?
int[] arr = new int[]{1, 2, 3};
for (int i : arr) {
System.out.println(i);
}
foreach属于增强型的for循环,它使得代码更简洁,同时我们能直接拿到数组中的每一个数字。
二维数组
二维数组其实就是存放数组的数组,每一个元素都存放一个数组的引用,也就相当于变成了一个平面。
//三行两列
int[][] arr = { {1, 2},
{3, 4},
{5, 6}};
System.out.println(arr[2][1]);
二维数组的遍历同一维数组一样,只不过需要嵌套循环!
int[][] arr = new int[][]{ {1, 2},
{3, 4},
{5, 6}};
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 2; j++) {
System.out.println(arr[i][j]);
}
}
多维数组
不止二维数组,还存在三维数组,也就是存放数组的数组的数组,原理同二维数组一样,逐级访问即可。
可变长参数
可变长参数其实就是数组的一种应用,我们可以指定方法的形参为一个可变长参数,要求实参可以根据情况动态填入0个或多个,而不是固定的数量
public static void main(String[] args) {
test("AAA", "BBB", "CCC"); //可变长,最后都会被自动封装成一个数组
}
private static void test(String... test){
System.out.println(test[0]); //其实参数就是一个数组
}
由于是数组,所以说只能使用一种类型的可变长参数,并且可变长参数只能放在最后一位!
实战:三大基本排序算法
现在我们有一个数组,但是数组里面的数据是乱序排列的,如何使它变得有序?
int[] arr = {8, 5, 0, 1, 4, 9, 2, 3, 6, 7};
排序是编程的一个重要技能,掌握排序算法,你的技术才能更上一层楼,很多的项目都需要用到排序!三大排序算法:
- 冒泡排序
冒泡排序就是冒泡,其实就是不断使得我们无序数组中的最大数向前移动,经历n轮循环逐渐将每一个数推向最前。
- 插入排序
插入排序其实就跟我们打牌是一样的,我们在摸牌的时候,牌堆是乱序的,但是我们一张一张摸到手中进行排序,使得它变成了有序的!
- 选择排序
选择排序其实就是每次都选择当前数组中最大的数排到最前面!