java基础知识学习小总结
什么是java
Java是一门面向对象编程语言,不仅吸收了C++语言的各种优点,还摒弃了C++里难以理解的多继承、指针等概念,因此Java语言具有功能强大和简单易用两个特征。Java语言作为静态面向对象编程语言的代表,极好地实现了面向对象理论,允许程序员以优雅的思维方式进行复杂的编程 。
java的特性和优势
既然我们选择学习java,那么我们应该有去学习它的理由,不仅仅是因为它可以赚贼鸡八多的钱,可能更是因为它简单易上手而且它的特性在现在这个网络环境中非常吃香,且恐怕会火热持续相当多年,未来可期,那我们至少要了解它的一些特性,至少我们要对自己明确我们学习它的价值到底值不值得,要不要继续往下精学,而不是大家选择java,而我就去选择java,需要有自己独立的判断能力。1.简单性
java可以说是C++的纯净版,没有头文件,没有指针运算,而且语法基于C,学习起来相比C++和C会比较轻松一点。
2.面向对象
了解面向对象之前,我们先了解一下面向过程。两种都是解决问题的一种思想。
面向过程思想
步骤清晰且具体化,思考问题都放在每一步具体的实现上,第一步做什么,第二步。比如你要去上个厕所,得先站起身,走出门,进厕所,脱下裤子,开吃。他适合处理一些简单的问题,但是如果你用这种思想去解决一些相对复杂的问题,比如你要造一座高楼,你一个人去造这个高楼,可能也能造出来,可是相对的代码的复杂程度以及耦合度可能会很高,也相对的难以解决,毕竟一个人要去做所有事情在整个过程里。
面向对象思想
而面向对象思维,是一种分类的思维方式来解决问题,同样以建造一个高楼举例,我会找一个总设计师,总设计分配任务给下面的人,比如做地基的人,装修的人,刷墙的人等等,相当于把这个过程进行细分,刷墙的人可能还会继续往下细分成刷外墙的人和刷内墙的人,而细分出来类型的人,比如刷外墙的人所做的事情,其实也就是一个面向过程的实现。但是总体的架构可以通过这种思想变得非常的清楚。思考问题首先是先思考这个问题应该怎么分类,然后对这些分类进行单独的思考,还是以上厕所为例子,人本身就是一个对象,他有一个方法叫做上厕所,至于中间的拉屎过程在上厕所这个方法里面进行具体的面向过程的实现就好了。这样也易于分工合作,可将每个分类出来的单独思考分配给不同的人,共同完成。面向对象适合处理复杂的问题,适合处理需要多人协作的问题,这也是为什么java能做出那么多大型系统软件的一个核心,能多人一起共同解决。具体的讲解及结合代码的讲解下面会总结
3.可移植性(平台无关性)
所谓平台无关性就是说我的一份代码写好之后,可以在不同的硬件平台上运行,也就是跨平台性。平台无关性有两种:源码级别和目标代码级别的
源码级别
C/C++语言是源码界别的。这两种语言属于编译型语言。只要有源代码,在不同平台上重新进行编译就可以实现跨平台运行。但是你在交付项目代码的时候,我们一般是交付软件产品,不会把源码交出去,防止自己产品的核心代码被泄露,那这个时候,这种软件就只能在特定的平台上运行了
目标代码级别、
Java是目标代码级别的,因为java是一种解释型语言,写好的java代码编译会形成字节码文件,去到哪个平台都能用,只要在那个平台安装JVM虚拟机,虚拟机会根据你所在的平台,选择不同平台的解释器把Java编译的字节码解释为不同的机器码,达到在不同平台运行的效果。一次编译,到处运行
在《JAVA核心技术卷一:基础知识》中描述到:“JAVA并不只是一种语言。在此之前出现的那么多种语言也没用能够引起那么大的轰动。Java是一个完整得平台,有一个庞大的库,其中包含了许多可重用的代码和一个提供诸如安全性、跨操作系统得可以执行以及自动垃圾收集等服务的执行环境。”
关于反编译
所谓反编译就是可以根据class文件反向解析成原来的java文件。因为使用javac编译不像C/C++编译时那样进行加密和混淆,它是直接对其进行符号化、标记化的编译处理,所以很轻易的就能解析出来。
java从出现开始,就十分提倡开放共享,这也是java迅速发展壮大深受广大程序猿喜爱的一个原因。但许多时候,公司为了商业技术的保密考虑等等情况,会不希望可以直接反编译成源码出现源码泄露的情况。那么就需要进行加密措施,这种情况采取的手段也很多:代码混淆,加class(ClassLoader),高级加密class,甚至是直接修改JVM,这些有兴趣的可以百度了解下。
4.高性能
Java三高,我就不多描述了。我想介绍一下东西,叫做即时编译器(JIT),它甚至可以在执行的时候提高性能。
当JVM的初始化完成后,类在调用执行过程中,执行引擎会把字节码转为机器码,然后在操作系统中执行。在字节码转换为机器码的过程中,虚拟机还存在一道编译,那就是即时编译。最初,JVM中的字节码由解释器完成编译,当虚拟机发现某个方法或者是代码块的运行特别频繁,就会把这些代码认定为热点代码,那么为了提高这些代码二执行效率,在运行的时候,JIT会把这些代码编译成和本地平台相关的机器码,然后经过一系列的优化之后,保存到内存中,以此提高执行代码的性能,所以如何去编写代码和设计代码,其实考虑的东西随着学习的知识越来越多,会慢慢的需要考虑的更多,希望终有一天我们都能写出性能极佳,一眼看出代码背后的所有故事。那个时候希望我们可以一起拉屎聊代码。
5.分布式
java可以说是为网络分布式环境所设计的,因为它能够处理很多TCP/IP协议上的东西。除此之外,Java还有一个东西叫RMI(远程方法调用),考虑一个问题,在一个分布式的环境,如果一台机子上的对象调用了另外一台机子上的对象的方法,该如何解决,这还不是另外一台机子上的资源,而是另外一台机子上的虚拟机里的代码?我就不多说了,因为我也不会啊,好奇的可以去了解一下。
6.动态性
java是静态语言,但是因为反射机制,java变成了一个准动态语言,这也是c++所没有的东西。什么叫动态语言,就是在程序运行时,可以改变程序结构或变量类型。而反射机制是指可以在运行时加载使用编译期里完全未知的类,因为在类刚开始加载的时候,堆中就已经产生了一个Class类型的对象,这个对象相当于是一个模板类,里面包含了一个类的完整的类结构信息,且一个类只能有一个Class的对象,而通过这个Class对象,我们就能得到这个类的方法和属性等等,像一面镜子一样,所以我们称它为反射,获取到了这个Class我们就可以进行动态的创建实例调用方法修改和查看属性等等操作了,具体的反射会在下面进行详细说明。
7.多线程
可以并行执行多项任务,可以带来更好的交互行为和事实行为,这就不多说了,线程下面也会详细说明。
8.健壮性
Java语言的强类型机制、异常处理、垃圾的自动收集等
9.安全性
java提供了一个安全沙箱来保证网络或者其他不信任的地方下载并运行程序不会破坏到本地数据,java的安全模型主要从jvm对外的防护,防止jvm被恶意侵入获取特定权限以及对jvm内部的防护。具体东西太多了,推一篇博客。
https://blog.csdn.net/shendeguang/article/details/8213960
Java的三大版本
Write one run anywhere 什么叫虚拟机,也就是虚构出来的计算机,这个虚构的计算机几乎可以跑在所有的平台上,所以可以实现跨平台,应用也就相当广泛。JavaSE :标准版(桌面程序,控制台开发) SE是整个java的核心和基础。
JavaME: 嵌入式开发(手机,小家电)
JavaEE: 企业级开发(web,服务器端开发) EE运用的目前最为广泛,它提供了很多企业级应用开发的完整的解决方案。
JDK,JRE,JVM之间的关系
顾名思义,JDK开发库,JRE运行时库,JVM Java虚拟机JRE可以支撑Java程序的运行,JRE包含了JVM(java.exe)以及基本的核心类库(rt.jar)也就是说,如果仅仅是运行java程序,可以不装JDK,装一个JRE就可以了。
JDK可以支持Java程序的开发,包括了编译器(javac.exe),开发工具javadoc.exe、jar.exe、keytool.exe、jconsole.exe)和更多的类库(如tools.jar等)来更好的帮助开发,当然jdk也包含了jre。
下面是Oracle官方的一张图,有兴趣的可以去了解一下https://docs.oracle.com/javase/7/docs/
Java程序运行机制
在计算机的高级语言类型,分为两种:编译型和解释型。
什么是编译型和解释型?比如现在有一个外国人要读一本刘慈欣写的《三体》,但是这个外国人看不懂中文,那么他可以选择买一本把整个三体编译成英文的一本书,而这种就是编译型,编译成你看的懂得语言,你也可以选择请一个翻译,你读到哪里,我给你翻译到哪里,解释给你听,这种就是解释型。相对于计算机而言编译型就是我现在用java这个高级语言,我需要编译成计算机能够直接读懂的语言(其实就是机器语言),让计算机直接去执行。或者选择我一句一句把我写的代码解释给计算机听,然后计算机一句一句去执行。编译型的好处在于,我不需要你给我解释我能更快更简单的读懂执行,所以对速度要求比较高的例如操作系统之类的,一般就会选用编译型语言去做。而解释型语言的特点在于灵活,比如外国人看的那本翻译的书有问题,如果作者想去改变,那么整本书相当于要重新翻译发布,而你如果选择找个人解释翻译给你听,它可以直接指出更改的内容,而Java是属于即是编译型又是解释型的语言。
Java程序运行的机制如图
如上图所示,我们所写的代码也就是源文件,会先经过一次编译,也可以理解为预编译,生成了一个class的字节码文件,这个字节码文件是介于java代码和机器码中间的,然后将这个class文件放入jvm中,在jvm中经过装载校验,然后jvm会通过解释器解释给操作系统也就是计算机听,然后进行执行。
Java的基础语法
注释
什么是Java注释:用于解释说明程序的文字单行注释
格式: // 注释文字
多行注释
格式: /* 注释文字 */
文档注释
格式:/** 注释文字 */
注释是不会进行编译的,但是文档注释可以通过javadoc生成注释文档,我们的jdk文档也是通过生成注释文档的来的,一个好的注释代码的习惯可以有效的提高我们代码的可读性,下面介绍一种idea生成注释文档的方法。
package com.jj.notes;
/**
* @author 作者名
* @version 版本号
* @since 指定需要的最早使用的jdk版本
*
*
*/
public class Test {
/**
* @return
* @param name
* @throws Exception
*/
public String test (String name)throws Exception{
return "test";
}
}
如图所示的代码,在idea里面选择Tools——>Generate JavaDoc
在如上图需要填入以下内容,Output directory填入文档生成输出的路径,点击OK。-encoding UTF-8 -charset UTF-8
完成之后可以在输出路径看到如下文件,点击index进主页(一般生成了之后会自动弹出主页的)
可以和jdk帮助文档进行对比,如果用心做一个项目,可以考虑维护这种帮助文档,jdk帮助文档如下。
标识符
标识符,通俗易懂就是名字,Java所有的组成部分都需要名字。类名,变量名,方法名等等都被称为标识符标识符命名规范
所有的标识符都应该以字母A-Z或者a-z,美元符$或者下划线_开始
首字符之后可以是字母,美元符,下划线或者数字的任何字符的组合。
不能使用关键字作为标识符
标识符是大小写敏感的(意思是比如关键字class,你取个名字叫Class,cLass,CLAss也是可以的,但是一般不建议那么做)
可以使用中文名,但是一般并不建议去使用,更不建议使用拼音命名,取名字最好就是见名知意,增加可读性。
基本数据类型
java是强类型语言,一般也叫做强类型定义语言,要求变量的使用严格符合规定,对于每一种数据都定义了明确的具体的数据类型,在内存中分配了不同大小的内存空间
基本数据类型
public static void main(String[] args) {
//整数:进制 二进制0b 十进制 八进制0 十六进制0x
int i =10;
int i2=0b10;
int i3=010;
int i4=0x12;
System.out.println(i);
System.out.println(i2);
System.out.println(i3);
System.out.println(i4);
//输出结果为 10,2,8,18
}
浮点数使用问题
浮点数是不精确的,有涉及小数的操作,最好完全避免使用浮点数进行比较,尤其是银行业务!原因大家来看下面这段代码
public static void main(String[] args) {
float f=0.1f;
double d=0.1;
System.out.println(f==d);//输出结果为false
float f1=232311131161613131313131f;
float f2=f1+1;
System.out.println(f1==f2);//输出结果为true
}
为什么浮点数不精确,其实这句话本身就不精确,相对精确一点的说法是:我们程序员在程序里写的十进制的小数,计算机内部无法用二进制的小数来精确的表达。
对于二进制小数,小数点右边能表达的值是1/2,1/4,1/8…1/(2^n)
比如我现在来表达一下十进制的0.2
0.01 = 1/4 = 0.25,太大了
0.001 =1/8= 0.125,又太小了
0.0011=1/8+1/16=0.1875,接近0.2了
0.00111 = 1/8 + 1/16 + 1/32 = 0.21875 , 又大了
0.001101 = 1/8+ 1/16 + 1/64 = 0.203125 还是大
0.0011001 = 1/8 + 1/16 + 1/128 = 0.1953125 这结果不错
0.00110011 = 1/8+1/16+1/128+1/256 = 0.19921875 哇这个好逼近啊 决定就是他了
这就是所说的二进制小数无法精确表示十进制的小数,关于小数的精确计算,推荐使用BigDecimal来做精确运算。
类型转换
//类型转换
public class Test02 {
int i=128;
byte b=(byte)i;//内存溢出,强制转换,高到低
double d=i;//自动换转 低到高
/**
* 低——————————————————>高
* byte,char,short——>int——>long——>float——>double
* 虽然float是三十二位,但是小数的优先级高于整数
* 高到低强制转换,低到高自动转换。
* 不能对布尔值进行转换
*/
}
这里为什么要把char也放进来呢,因为char实际上也是一个数字,所有的字符本质上还是数字,如下图代码
public static void main(String[] args) {
char c1='c';
System.out.println(c1);
System.out.println((int)c1);
char c2='国';
System.out.println(c2);
System.out.println((int)c2);
}
char字符有一张Unicode编码表,每一个数字对应了表中的一个字符,所以字符的本质其实还是数字,正常情况下还有一种\u0000的写法,\u代表的是转译的意思
类型转换注意点
public static void main(String[] args) {
//操作数比较大的时候,注意溢出问题
//jdk7新特性,加下划线不影响数字表达
int money=10_0000_0000;
int years=20;
int total=money*years;//直接溢出 输出-1474836480
long total2=money*years;//依然溢出,说明可能在转换以前就已经存在问题
//输出正常,说明计算之前,把表达式都提升成了long类型进行计算,说明前两个计算先将
//两个表达式进行int类型计算,然后再进行提升,但是计算的时候就已经发生了溢出问题
long total3=money*(long)years;
System.out.println(total3);
}
引用数据类型讲个鬼。
变量
变量是什么,不就是变化的量嘛。Java是一种强类型语言,所以每个变量都必须声明其类型。
Java变量是程序中最基本的存储单元,其要素包括变量名,变量类型和作用域。
type varName [=value]
每个变量都有类型,类型可以是基本类型,也可以是引用类型。变量名必须是合法的标识符。
变量声明是一句完整的代码,因此每一个声明都必须以分号结束.
public class Test {
int a; //属性:变量;实例对象:从属于对象,如果不自行初始化,会赋值为这个类型的默认值
static double salary=2500;//类变量 static
public static void main(String[] args) {//main方法
int a;//局部变量,必须声明初始值
}
public void test(){ }//其他方法
}
具体的变量的区别,会在后面内存分析的时候再说明
变量的命名规范
所有的变量,方法名,类名:见名知意
类成员变量:首字母小写和驼峰原则:monthSalary
局部变量:首字母小写和驼峰原则
常量:大写字母和下划线:MAX_VALUE
类名:首字母大写和驼峰原则:GoodMan
方法名:首字母小写和驼峰原则:runRun()
运算符
运算时类型问题
public static void main(String[] args) {
byte b=20;
short s=20;
int i=20;
long l=20;
System.out.println(b+s+i+l);//只要运算中有long类型,则返回long类型
System.out.println(b+s+i);//只要运算中有int类型(没有long类型),则返回int类型
System.out.println(b+s);//只要运算中没有long和int类型,则返回int类型
}
短路运算
计算机在进行运算的时候如果提前确认了结果,会直接将语句短路,执行下一条,比如boolean flag=(c<4)&&(c++<4);
,如果c>4那么结果必为false,那么后面的c++<4将不再执行,直接返回结果为false,所以如果在这种情况做操作需要考虑一下。
位移运算是最有效率的计算方式。
在计算机中最快的办法就是位运算,当问你2*8最快的运算办法是想到2*8=2*2*2*2,相当于把2在二进制中左移三位,因为在二进制中,左移相当于乘2,右移相当于除2,故写为2<<3
字符串连接符的注意点
public static void main(String[] args) {
int a=10;
int b=20;
System.out.println(""+a+b);//这种情况当作了字符串来处理,也就是将a+b粘在一块,输出1020
System.out.println(a+b+"");//这种情况却当作了数字处理,并不是当字符串处理,也不是先相加再变字符串,输出int类型的30
}
流程控制
顺序结构
Java的基础结构就是顺序结构,除非特别指明,否则就按照顺序一句一句执行。
顺序结构是最简单的算法结构
选择结构
if不知道能讲什么。if单选择结构
if多选择结构
嵌套if结构
过
switch多选择结构
多选择结构还有一个实现方式就是switch case语句
switch case语句判断一个变量与一系列中某个值是否相等,每一个值称为一个分支。
switch语句的变量类型可以是 byte short int或者char,到JavaSE 7开始,switch还支持字符串String类型
同时case标签必须为字符串常量或字面量
switch()括号内填入的值如果不是String类型,则刚刚所提到的可填入的变量类型除了String之外都可以写进case里,但如果填入的是一个String类型,那么case所可以匹配的值都必须是String类型
swtich更多时候是用来匹配一个值,但是匹配的值需要加上break; 否则就会出现穿透现象,
比如case a,b,c 如果匹配的值为b 他会从b开始穿透,没有遇到break的情况会从b开始执行到c一直执行到结束为止(如果没有break)
public static void main(String[] args) {
char c='B';
switch (c){
case 'A':
System.out.println(13);
break;
case 'B':
System.out.println(13);
case 'C':
System.out.println(14);
case 12:
System.out.println(15);
}
//输出结果为 13 14 15
}
注意点:如果我在匹配一个String字符串的时候,传入了一个空的字符串,会发生什么事,会报错吗?
public static void main(String[] args) {
String c=null;
switch (c){
case "A":
System.out.println(13);
break;
case "B":
System.out.println(13);
case "C":
System.out.println(14);
case "12":
System.out.println(15);
}
}
会报错,会直接报NullPointerException错误,为什么会报错,我们可以把这个代码生成的class文件进行反编译,可以得到如下代码。
public static void main(String[] args) {
String c = null;
byte var3 = -1;
switch(((String)c).hashCode()) {
case 65:
if (((String)c).equals("A")) {
var3 = 0;
}
break;
case 66:
if (((String)c).equals("B")) {
var3 = 1;
}
break;
case 67:
if (((String)c).equals("C")) {
var3 = 2;
}
break;
case 1569:
if (((String)c).equals("12")) {
var3 = 3;
}
}
switch(var3) {
case 0:
System.out.println(13);
break;
case 1:
System.out.println(13);
case 2:
System.out.println(14);
case 3:
System.out.println(15);
}
}
可以发现,他们去匹配这个字符串的时候,实际上匹配的是hashcode的值,所以将null进行hascode方法的时候自然会报NullPointerException
循环结构
while必须满足条件,条件表达式只要一直为true就一直执行。
dowhile至少执行一次
break强制退出整个循环,continue跳过本次循环,如果有多层循环想一次性跳出,可以在外面的循环语句前定义一个标号,代码如下
public static void main(String[] args) {
ok://标号
for (int i = 0; i < 10; i++) {
System.out.println(i);
for (int i1 = 0; i1 < 5; i1++) {
System.out.println(i1);
break ok;//break这个标号,两层直接跳出,break只能跳出一层循环
}
}
}
增强for循环,可以直接遍历数组。public static void main(String[] args) {
int[] i={1,2,3,4};
for(int j:i){
System.out.println(j);
}
}
方法
Java方法是语句的集合,它们在一起执行一个功能。比如人是一个类,上厕所是人的一个方法。
设计方法的原则:方法的本意是功能块,就是实现某个功能的语句块的集合。我们设计方法的时候,最好保持方法的原子性,也就是一个方法只完成一个功能,这样利于我们后期的扩展。
return会直接终止方法。
方法调用有两种方式,根据方法是否返回值来选择,当方法有返回值的时候,通常被当作一个值。如果方法返回void,方法调用一定是一条语句。
那么,方法中的参数是引用传递还是值传递呢?
介绍一下形参和实参
形参是指定义函数名和函数体所用的参数。public void test(String a)
实际参数指调用函数填入括号内的参数。test(“字符串”)
。两个之间存在拷贝关系。
java的传递方式
**Java中实际上都是值传递**。无论你放进去的是一个值,还是一个地址,都只是值传递,也都只是拷贝关系。public class Test01 {
int c=3;
String d="a";
public static void main(String[] args) {
int a=3;
String b="a";
Test01 test01=new Test01();
test01.test(a,b);
System.out.println(a);//输出结果仍为3
System.out.println(b);//输出结果仍为a
test01.test2(test01);
System.out.println(test01.c);//输出结果为4
System.out.println(test01.d);//输出结果为b
}
public void test(int a,String b)
{
a=4;
b="b";
}
public void test2(Test01 test01){
test01.c=4;
test01.d="b";
}
}
test方法里进行了赋值操作,main方法的a,b没有变化,这也说明了这是一个拷贝的数值,并不是直接修改了源数值。他是将入参复制了一份,进行操作的,完全不影响源数值。那么引用呢,test2就是一个传入test的对象引用,输出结果修改值成功了,那是不是java即是值传递,也是引用传递呢,其实还是只是一个值传递,因为他只不过是复制了你得地址位置。你得地址相当于一把钥匙的话,那么复制你得地址相当于是复刻了一把新的钥匙,这不能算是引用传递,你并没有把原本的钥匙给我,所以,值传递和引用传递的区别并不是传递的内容。而是实参到底有没有被复制一份给形参。
还有一个点就是上面的String也是引用类型,为什么没有改变值,正常来说引用类型应该是传入了一个地址的。因为地址的Test类对象地址输出实际上是toString方法的输出,但是String内部重写了toString方法,输出的不会是地址,而会是值,相当于把输出的值传递进了方法里进行拷贝,这也从侧面反映了java确实是值传递,并没有将引用传递。
方法的重载
重载方法就是在一个类中,有相同的函数名称,但形参不同的函数。
方法重载的规则:
方法名称必须相同。
参数列表必须不同(个数不同,类型不同,参数排列顺序不同)。
方法的返回类型可以相同可以相同。
仅仅返回类型不同无法构成方法的重载。
方法名称相同的时候,编译器会根据调用方法的参数个数,参数类型等去逐个匹配,已选择对应方法,如果匹配失败,编译器报错。
递归
看代码 public static void main(String[] args) {
System.out.println( f(4));
}
public static int f(int n)
{
if(n==1){
return 1;
}else {
return n*f(n-1);
}
}
这是一个简单的递归,计算的4的阶乘,过程分析如下
深入的递归就不说了,因为还在复习基础,如果有兴趣深入的,阶乘、斐波那契数列、汉诺塔、杨辉三角的存取、字符串回文判断、字符串全排列、二分查找、树的深度求解八个经典问题都可以去了解一下。
数组
数组是相同类型数据的有序集合数组描述的是相同类型的若干个数据,按照一定的先后次顺序排列组合而成的。
其中,每一个数据称作一个数组元素,每个数组元素可以通过一个下标来访问他们。
数组的四个特点
数组的长度是确定的,数组一旦被创建,它的大小就不可以改变
数组元素必须是相同类型,不可以出现混合类型。
数组中的元素可以是任何数据类型,包括基本类型和引用类型。
数组变量属于引用类型,数组也可以看成对象,数组中的每个元素相当于该对象的成员变量。
数组本身就是对象,Java中对象是在堆中的,因此数组无论保存原始类型还是其他对象类型,数组对象本身是在堆中的。
数组内存分析
当声明int[] array;
的时候,array会被压入栈内,此时array的值是空的,只有当array=new int[5];
的时候,因为数组也是一个引用类型,或者说是一个对象,会在堆中开辟一个array数组的空间,这里定义的数组长度为5,一旦定义长度之后不可改变,数组在这个时候会进行默认初始化,也就是int的数组,会赋值给数组每一个元素int类型的默认值0。
下列是初始化的三种方式
public static void main(String[] args) {
//静态初始化:创建+赋值
int[] a={1,2,3,4,5,6,};
//动态初始化:包含默认初始化
// (默认初始化的意思是开辟了一个十空间的数组,数组元素都赋了一个默认值,int类型默认值为0)
int[] b=new int[10];
b[0]=10;
}
二维数组
二维数组就和套娃一样,多维数组就是无限套娃。二维数组就是一个数组里的每一个元素都是一个数组,所以你在int[][]
用new初始化的时候可以先不分配元素上的数组的长度,也就是可以int[][] a=new[10][]
new还是一样会赋给每个元素初始值,每个元素都是数组,数组是引用类型,初始值为null。
数组的排序(几种常见排序)
冒泡排序
比较相邻的两个元素,如果第一个比第二个大,就移到后面去,假设冒泡从数组第一个元素开始进行比较,一直比较到最后一个,那么必定会产生一个数组的最大值,如果重复这个过程,遍历情况最差的时候应该是重复遍历了数组长度-1的次数,而我们判断停止遍历的条件应该是,他们遍历一次不再进行交换,说明顺序已经正确,无需继续往下遍历。
代码实现
public class BubbleSort {
public static void main(String[] args) {
int[] arr={4,4,2,5};
bubbleSort(arr);
System.out.println(Arrays.toString(arr));
}
private static void bubbleSort(int[] arr){
//冒泡排序最坏的情况需要n-1次,排序可停止的条件为不再进行交换。
for (int i = 0; i < arr.length-1; i++) {
boolean flag=false;//作为标志位,减少没有必要的比较
for (int j = 0; j < arr.length-1; j++) {
if(arr[j]>arr[j+1]){
swap(arr,j,j+1);
flag=true;
}
}
if(flag==false){
break;//如果一次遍历完全没有进行交换,说明顺序正常,不需要进行再次遍历
}
}
}
private static void swap(int[] arr,int left,int right){//数组的两个元素位置进行交换
int temp=arr[left];
arr[left]=arr[right];
arr[right]=temp;
}
}
选择排序
用第一个元素开始和后面比较,如果存在比第一个元素小的,那么交换位置,那么每一轮至少保证一个元素是全组最小的,然后再开始第二个元素,直到n-1个元素代码实现
public class SelectSort {
public static void main(String[] args) {
int[] arr={2,1,6,3,8};
selectSort(arr);
System.out.println(Arrays.toString(arr));
}
private static void selectSort(int[] arr){
//选择排序比较的轮数为n-1次,每一轮都会比出一个最小的数在最前面
for (int i = 0; i < arr.length-1; i++) {
//判断为第几轮,来确定需要比较的次数
for (int j = i; j < arr.length-1; j++) {
if(arr[i]>arr[j+1]){
swap(arr,i,j+1);
}
}
}
}
private static void swap(int[] arr,int i,int j){//数组元素交换位置
int temp=arr[i];
arr[i]=arr[j];
arr[j]=temp;
}
}
插入排序
从第一个元素开始,取下一个元素,对前面进行比较,如果比前面的元素小,则插入到该元素的前面,这种比较是可以依次进行的,比如一组数3,2,1,0。第四个数进行插入的时候,比较前一个数,比1小,则会继续比较,你可以想象成相对应的1移到了0的位置,但是0还未插入,继续往前和2比较,2移到了1的位置,还未插入,继续往前和3比较,3移到了2的位置,然后0插入到3的位置。代码如下
public class InsertSort {
public static void main(String[] args) {
int[] arr={3,2,1,0};
insertSort(arr);
System.out.println(Arrays.toString(arr));
}
private static void insertSort(int[] arr){
//外层循环次数,从0的下一个元素开始
for (int i = 1; i < arr.length; i++) {
int j=i;
//循环终止条件为比较到大于前一位为止,且下标值需要大于零。
while (j>0&&arr[j]<arr[j-1]){
swap(arr,j,j-1);
j--;//继续往前比较
}
}
}
private static void swap(int[] arr,int j,int jLast){
int temp=arr[jLast];
arr[jLast]=arr[j];
arr[j]=temp;
}
}
希尔排序
希尔排序相当于是优化版的插入排序。因为其实直接插入排序就是增量为1的希尔排序,那么什么是希尔排序呢。
首先选取一个增量,假设容量为8的一个数组,增量我取4,然后增量作为分组,分成四组,间隔也为4,比如数组数是1-8,那么1,5为一组,2,6为一组依次类推,然后对每一组进行插入排序,排序结束后,增量再减半,直到增量为1时做最后一次排序。写代码的难点在于这种分组的排序,但可以在一个循环内做到,重点就是在于你比较和交换数的时候如何不影响其他分组的值。
代码如下
public class ShellSort {
public static void main(String[] args) {
int[] arr = {2, 3, 4, 1, 11, 8, 9, 0};
shellSort(arr);
System.out.println(Arrays.toString(arr));
}
private static void shellSort(int[] arr){
//增量减半不断循环,直到增量到1的时候进行最后一次比较
for (int h = arr.length/2; h >0; h/=2) {
//因为分组的关系,间隔为增量,插入排序是从第一个元素的下一个元素开始的,下一个元素为第一个元素的位置+增量
for (int i = h; i < arr.length; i++) {
int j=i;
boolean flag;//插入排序临界条件,如果与前一个值比较都不换位,则可以直接比较了
//所以至少比较一次
do{
flag=false;
if(arr[j]<arr[j-h]){
swap(arr,j,j-h);
j-=h;//防止分组被打乱,选择正确分组的元素进行比较
flag=true;
}
}while (flag&&j-h>0);
}
}
}
private static void swap(int[] arr,int j,int jNext){
int temp=arr[j];
arr[j]=arr[jNext];
arr[jNext]=temp;
}
}
希尔排序,缩小增量排序,他是对插入排序的一个优化,核心的思想就是合理的选取增量。
//克努特序列如果对于大的数据更佳,h增量采用 h=(h-1)/3的递减 而初始值采用
/*
int jiangge=1
while(jiangge<=arr.length/3){
jiangge=jiangge*3+1
}
*/
快速排序
以一个数为基准数,实现把小于此数的数放在左边,大于此数的数放在右边,再对左边和右边的进行再排序,这个再排序里又是一次快速排序,直到排序完全正确,有递归的思想。
具体操作为,选取一个数为基准数,将这个基准数挖出,存到pivot变量中,然后如果选择为左端,则从另外一端开始选择小于pivot的数,挖出,将数字存入原本pivoit的位置(因为pivot被挖出,实际上那个位置是为空的),填入之后开始从左端进行扫描找比pivot大的,然后将该数挖出,从另外一段开始继续扫描,选择比pivot小的数,把大的数填入(小于pivot的数那个坑)坑中,继续找小于pivot的数,然后挖坑。扫描停下来到另外一边的标准不是填坑,而是要找到和 基准数对比大或者小的目标数。
有个大佬讲的很清楚,可以看看
快速排序讲解
public class QuickSort {
public static void main(String[] args) {
int[] arr={5,4,7,2,9,1,77,8,3,21,66};
quickSort(arr,0,arr.length-1);
System.out.println(Arrays.toString(arr));
}
//可以选定范围进行快速排序,不一定是全部,所以设定一个left和right,也方便左右扫描
private static void quickSort(int[] arr,int left,int right){
//快排三步走,1.进行一次快排 2.对左边进行快排 3.对右边进行快排。一直进行下去
// (递归)直到顺序正确,判断顺序正确的(递归)边界为左扫描和右扫描扫描到了同一个位置
if(arr==null||left>=right||arr.length<=1){
return;
}
int left2=pattern(arr,left,right);
//对左边进行快排
quickSort(arr,left,left2);
//对右边进行快排
quickSort(arr,left2+1,right);
}
//一次快速排序,返回一个基准数的位置
private static int pattern(int[] arr,int left,int right) {
int temp = arr[left];//基准数,取得基准数为最左边的数
//1.根据基准数讲小于的放左边,大于的放右
while (left < right) {//执行这个过程的条件为右指针始终大于做指针
while (arr[right] > temp) {//右边未找到比基准数小的,一直往前找
right--;
}
if (left < right) {//再次确认右指针大于做指针,进行填坑
arr[left] = arr[right];
left++;//加1开始从左开始扫描
}
while (arr[left] < temp) {
left++;
}
if (left < right) {//找到再判断一次,然后开始填坑,往复循环,直到不满足左指针小于右指针
arr[right] = arr[left];
right--;
}
}
arr[right]=temp;//循环结束时,需要把左右指针同时指向的地方赋值上基准值,因为基准值相当于一开始被挖出来的。
return left;//返回一个left相当于下一个递归快排的基准值
}
}
堆排序
堆排序的利用堆的性质,父结点大于或小于子结点,得出根结点必定为最值,然后将最值拿出来,再把数据变成堆,再拿一个最值,一直拿到结束,那么排序也就相对应的结束。构成堆需要满足两个条件:
1.必须是一颗完全二叉树
2.父节点必须大于子结点
代码如下:
public class HeapSort {
// 一个数,然后将这个数砍掉,再造堆选第二个大的数,再选第三个,一直选完为止
//将这个数砍掉在这里做的操作是最后一个结点和根结点进行交换,然后下一次遍历,不遍历最后一个结点,就相当于砍掉了。
//父结点parent=[(o-1)/2] 左子结点 c1=2i+1 右子结点 c2=2i+2
//需要用到树的性质,其实数组完全可以表示
public static void main(String[] args) {
int arr[]={4,10,3,5,1,2};
//一直砍,一直减
for(int i=arr.length-1;i>=0;i--){
//1.造堆
buildHeap(arr,i+1);
//2.砍
swap(arr,0,i);
}
System.out.println(Arrays.toString(arr));
}
private static void buildHeap(int[] arr,int n){
int last_node=n-1;//求出最后一个结点
int parent_node=(last_node-1)/2;
//从最后一个叶子结点的父结点进行heapify,可以做到全部进行比较
//(heapify意思是对父结点和两个或者一个子结点进行比较,若父节点部不为最大值或最小值,那么进行交换,保证
// heapify之后的父结点为最大值),把数组当作数来看,依次一层往下,根结点就是0,第二层就是下标值1,2
// ,第三层就是3,4,5,6。通过从最后一个结点的父结点往上一直heapify到根结点,必定能保证选出整个数
// 的最大值。
for(int i=parent_node;i>=0;i--){
heapify(arr,i,n);
}
}
private static void heapify(int[] arr,int i,int n){
int c1=2*i+1;//左指结点
int c2=2*i+2;//右指结点
int max=i;//先假设父结点是最大的
if(c1<n&&arr[c1]>arr[max]){//<n确保有这个结点
swap(arr,c1,max);
}
if(c2<n&&arr[c2]>arr[max]){
swap(arr,c2,max);
}
}
private static void swap(int[] arr,int i,int j){
int temp=arr[i];
arr[i]=arr[j];
arr[j]=temp;
}
}
归并排序
将有序的两个数组,分成两个子序列数组,再对子序列数组进行归并排序(因为归并排序两个数组必须是排好序列的,那这种无限拆分至单个进行比较再递归回来必定是拍好序列的)。比如一个数组 1,2,3,4 分成了1,2 和3,4。再把1,2分成了1和2两个数组,进行比较,1小先放,再放2,形成1,2排序,3,4也一样。之后又到1,2和3,4进行比较,1大先放,然后又是2大,放2,然后是3,4。形成一个有序序列,这种无限分的子序列也一样是用到了递归的思想。代码如下
public class MergeSort {
public static void main(String[] args) {
int[] arr={3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};
mergeSort(arr,0,arr.length-1);
System.out.println(Arrays.toString(arr));
}
private static void mergeSort(int[] arr,int left,int right){
if(arr==null||arr.length<=1){
return;
}
sort(arr, 0, arr.length-1);
}
private static void sort(int[] arr,int left,int right){
if(left==right){//递归边界:直到同一个的时候退出递归
return;
}
int mid=left+((right-left)>>1);//一分为2
//对左边数组进行递归排序
sort(arr,left,mid);
//对右边数组进行递归排序
sort(arr,mid+1,right);
//合并两个数组
merge(arr,left,mid,right);
}
private static void merge(int[] arr,int left,int mid,int right){
//创建一个临时数字,作为合并数组。
int[] temp=new int[right-left+1];
int i=0;//temp数组从从零下表开始依次比较填入
int p1=left;
int p2=mid+1;
while (p1<=mid&&p2<=right){
temp[i++]=arr[p1]>arr[p2]?arr[p2++]:arr[p1++];//依次比较,直到一个数组全部填入合并数组
}
//将剩下那个数组的剩下的值依次放入合并数组中(数组保证有序)
while (p1<=mid){
temp[i++]=arr[p1++];
}
while (p2<=right){
temp[i++]=arr[p2++];
}
//临时数组赋值回给原数组
for ( i = 0; i < temp.length; i++) {
arr[left+i]=temp[i];
}
System.out.println(Arrays.toString(temp));
}
}
计数排序
计算排序就是记数字出现了多少次,然后进行排序。具体的步骤为:
1.找出待排序的数组的最大和最小的元素
2.统计数组中每个值为i出现的次数,存到数组的第i项
3.累加求出每个元素首次出现的下标
4.根据上面步骤所获得的数组信息,里面包含了计数的信息进行依次拿出。
代码实现
public class CountSort {
public static void main(String[] args) {
int[] arr={4,4,2,5};
countSort(arr);
System.out.println(Arrays.toString(arr));
}
private static void countSort(int[] arr){
int length=arr.length;
//判断最大值和最小值
int max=arr[0];
int min=arr[0];
for (int i = 0; i < length; i++) {
if(arr[i]>max){
max=arr[i];
}
if(arr[i]<min){
min=arr[i];
}
}
//新数组的长度
int offerSet=max-min+1;
//1.计数,在需要的数组长度上+1,因为数组的下标值是从0开始的,多一个0可以方便后续操作
int[] count=new int[offerSet+1];
//在+1索引的基础上记录频率,实际上就是记录2,3,4,5d的出现频率,
//那么count应该为 0 1 0 2 1 第二个零意思是三出现了0次
for (int i = 0; i < length; i++) {
count[arr[i]-min+1]++;
}
//计算元素开始位置的索引,往前一直累加,得到的数组
//为 0 1 1 3 4 前四位其实对应的就是2 3 4 5首次出现的位置,而3是不可能出现的,所以只能是4
//4首次出现的位置为数组下标的1,那么第二次出现就要进行+1了,也就是填入下标值为1的元素进数组之后就要对1进行++
for (int i = 0; i < offerSet; i++) {
count[i+1]+=count[i];
}
int[] aux=new int[length];//创建需要填入的数组
for (int i = 0; i < length; i++) {
aux[count[arr[i]-min]++]=arr[i];//填入需要加加 防止相等元素比如4再次填入同一个位置
}
for (int i = 0; i < length; i++) {
arr[i]=aux[i];
}
}
}
类和对象的创建
类是一种抽象的概念,他是对某一类事物整体的一个描述,定义,但是并不能代表一个具体的事物。而对象就是抽象概念的具体实例。比如人是一个类,而张三是一个具体的人。
构造器
一个类即使什么都不写,它也会存在一个方法,就是无参构造器。通过反编译你也可以看到编译的时候会给类加上无参的构造器,当然也可以自己显式的定义无参构造器。如果一个类里面定义了有参构造器,那么无参构造器就必须显式定义。
new实例化一个对象其实就是调用了类中的构造器方法。
创建对象内存分析
我们来看下面的代码进行分析public class Test02 {
public static void main(String[] args) {
Person xiaoming=new Person();//不推荐使用拼音命名,但是为了显示这是一个具体的人
xiaoming.name="小明";
xiaoming.age=3;
}
}
public class Person {
public String name;
public int age;
public void eat(){
System.out.println("吃饭");
}
}
下面是内存分析图,看着图,跟着文字一起走
1.程序加载,首先会加载有main方法的类进方法区,之前说过class相当于是一个模板,加载进方法,以及里面的常量,常量为“小明”,String类型是被final修饰的,在类中出现的字符串都会被作为常量存到该类的常量池中。 2.将main方法压入栈中,进行执行,(代码都是在栈中执行的,每调用一个方法,就会把方法压入栈中进行执行,也就是说根据先进后厨出的特性,main方法一定是最后出去的)
3.执行第一句 Person xiaoming=new Person(); 因为Person在方法区中未加载,所以会先将Person这个类加载进入方法区,然后在栈中申明了引用变量名 xiaoming,通过new这个关键字,在堆中创建出了xiaoming这个对象的内存空间,xiaoming指向该内存空间的地址,进行初始化,将name赋上null,age赋上0,eat()指向方法区中它对应的模板类Person的eat()。
4.执行第二句xiaoming.name=“小明”。xiaoming对应的地址空间中的name被赋值成小明,而这个小明是在Test02常量池中的,所以name会指向Test02常量池中的小明
5.执行第三句话 xiaoming.age=3,xiaoming对应的地址空间中的age被复制成了3。
静态方法区是和类一起加载的,static的问题后面会详解
面向对象三大特性
封装
封装:隐藏对象的属性和实现细节,仅对外提供公共访问方式,将变化隔离,便于使用,提高复用性和安全性。
说到底就是属性私有,get/set,禁止访问一个对象中的数据,而应该通过提供操作接口来访问。
继承
继承的本质是对某一批类的抽象。比如学生,老师类都是抽象类,他们都是人类,那么人类就是对这一批类的抽象,也就是抽象的抽象。
extends的意思是扩展。子类是父类的继承。
java中只有单继承,没有多继承,所有人类都继承object(特殊)
idea小快捷键,ctrl+h查看继承树
this关键字和super关键字注意点:
super:super只能出现在子类的方法中或者构造方法中,super代表的是父类对象的引用。
this:this代表调用者这个对象,而this在没有继承的情况下也能够使用,而super只能在继承关系的子类中才可以使用。
方法重写(其实也就是多态了):重写只存在方法的重写,和属性无关,属性可以(重名)扩展,但是不是重写。
而且重写只是重写非静态的方法。
如果我创建两个类 A为子类 ,B为父类,就算B b=new A()父类的引用指向了子类,这个在static的情况下,就算new A,如果有调用和父类同名的方法,默认还是会调用B的方法。
但是如果是非静态方法 那么B b=new A()会执行A中重写了父类的方法 。
private私有修饰符修饰之后,和继承都没有什么关系了,私有的就是只有自己才可以用,儿子都不行。
一个子类被加载,它的父类必定会先被加载。
多态
java实现多态需要满足三个条件:继承,重写,向上转型。
继承:需要存在子类和父类
重写:子类重写了父类的方法。
向上转型:需要将子类的引用赋给父类的对象。比如A为子类 B为父类 B b=new A();
此时用b去调用A有重写了B的方法的时候,会使用子类重写的方法。
这种机制使得引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,只有在程序运行的时候才能确定到底引用变量会指向哪个类的实例对象,引用变量发出的方法调用的是哪个类的方法只有程序运行的时候才能够决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。
留言
有点长,编辑器卡了,IO流,反射,static,多线程,代码内存详细分析,网络编程,String和hashcode,equeal的知识留到下一章节写。新人一枚,写的不好或者不对的地方,欢迎各位大佬指正。
强力推荐B站狂神说Java!!!!老师非常好,而且非常擅长拉屎。
本文地址:https://blog.csdn.net/weixin_44734093/article/details/109715246
上一篇: Vue到底如何实现数组响应式的
下一篇: 大部分人不会告诉你的12个JS技巧