java复习—基础篇(已完结)
文章目录
前言
此资料用于学过了,但还是有点欠缺或需要复习或是作为一份入门的资料使用,第一次做复习资料有不足如果有什么不对的地方请各位大佬指出(基本都是根据自身学习的东西来的)。
概念
什么是java?
Java是1995年由Sun公司推出的一门极富创造力的面向对象的程序设计语言,它是由有“Java之父”之称的Sun研究院院士詹姆斯.戈士林博士亲手设计而成的,正是他完成了Java技术的原始编译器和虚拟机。Java 最初的名字是OAK,在1995年被重命名为Java,并正式发布。Java是一种通过解释方式来执行的语言,其语法规则和C++类似。同时,Java 也是一种跨平 台的程序设计语言。用Java语言编写的程序,可以运行在任何平台和设备上,如跨越IBM个人电脑、MAC苹果计算机、各种微处理器硬件平台,以及Windows、UNIX、OS/2、 MACOS等系统平台,真正实现“一-次编写,到处运行”。Java非常适于企业网络和Internet环境,并且已成为Internet中最具有影响力、最受欢迎的编程语言之一。
java程序编译运行过程
java的优势
- 简单性
- 面向对象
- 可移植性
- 高性能
- 分布式
- 动态性
- 多线程
- 安全性
- 健壮性
简单性:
java语言简单明了,容易掌握,是纯面向对象的语言,其语法规则和C++类似又可以说是从C和C++演变而来的。java语言就像是C++纯净版,例如java使用接口取代了多重继承,取消了指针,内存的自动管理等等。
面向对象:
java提倡万物皆对象的原则,语法中不能再类外面定义单独的数据和函数,也就是说,java语言外部的数据类型是对象,所有的元素都需要通过类和对象来进行访问的
可移植性:
由于java在虚拟机内部运行,因此完全独立于底层的操作系统和硬件。能够在任何设备上运行,JVM屏蔽了底层平台的差异,对于不同的系统有不同的虚拟机,虚拟机提供硬件平台规范。java开发的程序被编译成虚拟机指令所形成的字节码,虚拟机将这些指令翻译成操作系统特定的指令,正因如此我们所编写的java代码可以在任何安装兼容虚拟机操作系统上运行。“一次编译,到处运行”。
高性能:
java编译后的字节码是解释器中运行的,所以它的速度较多速交互应用程序提高了很多,另外,字节码可以在程序运行时被翻译成特定平台的机器指令,从而进一步提高运行速度
分布式:
java分布性包括操作分布以及数据分布,其中操作分布是指在多个不同的主机布置相关操作,而数据分布是将数据分别存放在不同的主机上,java可以凭借URL对象访问网络对象、访问方式与访问本地系统相同。
动态性:
java可以动态调整库中的方法和增加变量,而客户端不需要进行更改。
多线程:
多线程应用程序能够同时运行多项任务,就比如你一边用QQ聊天一边打游戏这种。使用多线程,可以带来更好的交互能力和实时行为。
安全性:
java最初就是围绕智能设备之间的通信而设计的,对于java可用功能和禁用功能进行了非常严格的规定。java编程组件运行在不同的安全环境下,未经允许的java程序不会出现损害系统平台的行为。如:浏览器中运行的小程序不能访问web客户端底层文件系统,也不能访问除源站点以外的其他web站点。分布式组件也必须要经过运行才能与其他主机进行通讯。所以的java程序都运行在虚拟机上,这样可用防止底层操作受到侵害。
健壮性:
最初java只是一种Web页面添加交互性开发简单的通讯方案的简单机制然而,现在Java拥有许多引以为豪的强大功能和特性:可与最优秀的操作系统相媲美的图形用户界面(GUI) ;优于任何其他供应商的Web开发方案;领导市场的分布式企业平台;一套 极为丰富的、可实现数据输入和输出、多线程、高级图像处理、XML文档操作等功能的库集合。几乎所有能在一门]编程语言中找到的功能,在Java中都能找到。
java的三大版本
- JavaSE
- JavaME
- JavaEE
JavaSE:
标准版,java的基础以及核心一般用于桌面程序以及控制台开发。
JavaME:
微缩版,嵌入式开发,用于手机程序的开发之类的(基本凉了)。
JavaEE:
企业版:企业级开发,用于网站的开发、应用系统、服务器端的开发。
JDK、JRE、JVM
- JDK
- JRE
- JVM
JDK:Java Development Kit,Java开发者工具包,用来开发Java程序的,针对java开发者。。
JRE:Java Runtime Environment,Java运行环境,针对java用户。
JVM:Java Virtual Machine,java虚拟机 用来解释执行字节码文件(class文件)。
数据类型
java数据类型分为二种:
-
基本数据类型:有八种基本数据类型来存储数值、字符和布尔类型。如图
整数类型 数据类型 内存空间(8位等于1字节) 默认值 取值范围 byte(字节型) 8位 0 -128 ~ +127(-2⁷ to 2⁷-1) short(短整型) 16位 0 -2¹⁵ ~ +2¹⁵-1 int(整型) 32位 0 -2₃¹ ~ +2₃¹-1 long(长整型) 64位 0 -2⁶³ ~ +2⁶³-1 浮点数数据类型 float(单精度) 32位 0.0 +/-3.40282347E+38(6-7位有效数字) double(双精度) 64位 0.0 +/-1.7976931346231570E+308(15位有效数字) 布尔类型 boolean false 字符型 char \u0000
顺便附上一份ASCll字符码表格
ASCll字符对照表 | ||
---|---|---|
十进制 | 十六进制 | 对应字符(注意区分大小写) |
97 | 61 | a |
98 | 62 | b |
99 | 63 | c |
100 | 64 | d |
101 | 65 | e |
102 | 66 | f |
103 | 67 | g |
104 | 68 | h |
105 | 69 | i |
106 | 6A | j |
107 | 6B | k |
108 | 6C | l |
109 | 6D | m |
110 | 6E | n |
111 | 6F | o |
112 | 70 | p |
113 | 71 | q |
114 | 72 | r |
115 | 73 | s |
116 | 74 | t |
117 | 75 | u |
118 | 76 | v |
119 | 77 | w |
120 | 78 | x |
121 | 79 | y |
122 | 7A | z |
65 | 41 | A |
66 | 42 | B |
67 | 43 | C |
68 | 44 | D |
69 | 45 | E |
70 | 46 | F |
71 | 47 | G |
72 | 48 | H |
73 | 49 | I |
74 | 4A | J |
75 | 4B | K |
76 | 4C | L |
77 | 4D | M |
78 | 4E | N |
79 | 4F | O |
80 | 50 | P |
81 | 51 | Q |
82 | 52 | R |
83 | 53 | S |
84 | 54 | T |
85 | 55 | U |
86 | 56 | V |
87 | 57 | W |
88 | 58 | X |
89 | 59 | Y |
90 | 5A | Z |
-
引用数据类型:分为3部分,分别是类(class)、接口(interface)、数组。
- 类:
- Object:超类
- String:字符串
- Date:表示特定瞬间,精确到毫秒
- Void:一个不可实例化的占位符类,它保持一个对代表 Java 关键字 void 的 Class 对象的引用。
- 接口:接口(interface)是抽象方法和常量值的定义的集合,自己可以创建的就不进行一个赘述了。
- 数组:存储在一个连续的内存块中的**相同数据类型(引用数据类型)**的元素集合(这里就简单说明一下,后面会说的详细一点)。
- 类:
类型转换
- 强制转换
- 自动转换
强制转换:由高到底
自动转换:由低到高
注意点:
- 不能对布尔值进行转换
- 不能把对象转换为不相干的类型
- 把高容量转换低容量强制转换
- 转换可能存在内存溺出问题,或者精度问题
三元运算符
示例:
public void test(){
//语法:x ? y:z
//如果说x=true,则结果为y,否则为z
int count=80;
String type= count>60?"及格":"不及格";
System.out.println(type);
}
用户交互Scanner(这玩意我真忘掉了,还以为是什么新知识)
java.util.Scanner;
//基本的语法
public static void main(String [] args){
Scanner scanner=new Scanner(System.in);
System.out.println("使用next方式接收:");
//判断用户有没有输入字符串
if(scanner.hasNext()){
String str=scanner.next();
System.out.println("输出的内容为:"+str);
}
scanner.close();
}
Scanner是java5的新特性,用于接收用户的输入的信息
通过Scanner类的next()与nextLine()方法获取输入的字符串,在读取前一般使用hasNext()与hasNextLine()判断是否还有输入的数据
next():
1. 一定要读到有效字符才能输出
2. 对于输出有效字符串后空白才会去除,next()方法才会自动去除掉
3. next()不能有带空格的字符串
nextLine():
1. 以Enter为结束符,也就是说nextLine()方法返回的是输入回车之前所有字符
2. 可以获得空白
选择结构(这东西完全忘不掉的,可跳过这方面)
- if单选择结构
- if双选择结构
- if多选择结构
- 嵌套if使用
- switch多选择结构
这里我直接上代码了。。。
//if单选择结构
if(布尔表达式){
//如果表达式为true执行这里面的
}
//if双选择结构
if(布尔表达式){
//如果表达式为true执行这里面的
}else{
//如果上面的判断不为true则执行这里面的
}
//if多选择结构
if(布尔表达式 1){
//如果表达式1为true执行这里面的
}else if(布尔表达式2){
//如果表达式2为true执行这里面的
}else if(布尔表达式3){
//如果表达式3为true执行这里面的
}else{
//如果以上布尔值都不为true执行这里面的
}
//嵌套if使用
if(布尔表达式1){
//如果表达式1为true执行这里面的
if(布尔表达式2){
//如果表达式1和2都为true执行这里面的
}
}
//switch多选择结构
switch(需要判断的变量){
case 需要判断的数据1:
//执行内容
break;//没有break,会有case的穿透现象,要注意
case 需要判断的数据2:
//执行内容2
break;
default:
//以上判断都都不成立执行这里
}
循环结构(已经刻入DNA也是忘不了,跳过吧)
- while循环
- do_while循环
- for循环
直接上代码了
//while循环
while(布尔表达式){
//循环内容,别死循环了
}
//do_while循环
//不满足条件都需要至少执行一次
do{
//执行内容
}while(布尔表达式);
//for循环,我个人经常用的
for(初始化;布尔表达式;更新){
//循环内容
}
//扩展,增强版for循环,也是我个人经常用的
//主要用于遍历数组和集合
for(声明语句:表达式){
//代码内容
}
//示例
@Test
public void test2(){
int []count={10,20,30,40,50};//声明一个数组
for(int x:count){
System.out.println(x);//循环遍历
}
System.out.println(count[2]);//取第三位的值,因为下标是从0开始取值的
}
流程控制
break:用于任何循环语句主体部分,可以使用break控制循环流程,break用于强制退出,不执行剩余的语句
continue:终止本次循环,进入下一次循环
goto(了解):goto常被用于跳出多重循环。但goto 语句的使用往往会使程序的可读性降低,所以 Java 不允许 goto 跳转。
方法的重载
重载:就是一个类中有相同的函数名称,但是形参不同的函数。
规则:
1. 名称必须相同
2. 参数列表不同、个数不同、类型不同、排列顺序不同
3. 返回值可以相同可以不同,但是不足以成为重载的信息
数组
数组的定义:数组就是相同类型数据的有序集合,按照存入的先后顺序组合排列起来,每一个元素我们都可以用下标去进行访问。
数组的基本特点:
1. 长度是确定的,一旦创建就不能进行更改,大小也是不能改变的。
2. 数组中的类型可以是任意的数据类型,包括基本类型和引用类型。元素必须是相同类型,不允许出现混合。
3. 数组本身也是引用类型,数组也可以看作是对象,数组中每一个元素都可以看成对象的成员变量,数组的对象本身就在于堆中。
//数组定义
//变量的类型 [] 变量名称=变量的值;
//定义一个数组
int[] count;
count=new int[10];//定义范围10个数字
count[0]=1;//逐个赋值
count[1]=2;
count[2]=3;
count[3]=4;
count[4]=5;
count[5]=6;
count[6]=7;
count[7]=8;
count[8]=9;
count[9]=10;
for(int 1;i<count.length;i++){//获取数组下标
System.out.println(count[i]);
}
数组分为三种初始化方法:
-
静态初始化
int [] count={1,2,3} //数据类型[] 数组名称 = {值, 值, …};
-
动态初始化
int [] count=new int[2]; count[0]=1; count[1]=2
-
默认初始化
数组是引用类型,他的元素相当于类的实例变量,因此数组的一经分配空间,其中每个元素也会按照实例变量同样的方式被隐式初始化
多维数组
多维数组可以看成数组中的数组,比如二位数组就是一个特殊的一维数组,每一个数组都是一个一维数组
//二维数组
int count[][]={{1,2},{2,3},{3,4},{4,5}}
Arrays类
- 数组的工具类java.util.Arrays
- 具有一下这几种功能
- 给数组进行赋值:通过fill方法
- 对数组进行排序通过sort方法,按升序
- 比较数组:通过equals方法比较数组元素是否相等
- 查找数组元素:通过binarySearch方法对排序好的数组进行二分查找法操作
int [] a={5,1,2,3,4};
//利用Arrays工具类打印数组元素
System.out.println(Arrays.toString(a));
//排序输出,升序
Arrays.sort(a);
System.out.println(Arrays.toString(a));
//数组填充,将a中所有的元素用0填充
Arrays.fill(a,0);
//数组填充,将a中2到4的元素用0填充
Arrays.fill(a,2,4,0);
排序
java中一共有八大排序算法(基本都是自己找到,自己也就能写出个冒泡和简单排序直接插入这种。):
- 直接插入排序
- 希尔排序
- 简单选择排序
- 堆排序
- 冒泡排序
- 快速排序
- 归并排序
- 基数排序
直接插入排序:直接插入排序需要依次每次选出一个数据,插入到之前排序的数组的合适位置后,插入前要把该位置后面的数据都要后移一个位置,不断循环直到完全插入数据,完成排序。
/*
*实现直接插入排序
*/
//声明一个数组
int [] array={21,6,8,30,452,300,53,12,2,1,5};
public void insertion(int[] array){
int count=0;
int i;
int j;
for (i = 1; i < array.length; i++) {
count=array[i];//选中需要插入的数据将其往下推一格
for(j=i-1;j >= 0 && count < array[j];j--){
array[j + 1] = array[j];//将大于count的值整体后移一个单位
}
array[j + 1] =count;
}
System.out.println(Arrays.toString(array));//输出
}
希尔排序:希尔排序算是比较难理解一点的了,简单来说就是插入排序的另外一种方式,更加高效,将一组已经排序好了的数组插入进入另外一组排序好的数组。
//实现希尔排序,用到上面的数组
public void hill(int[] array){
//先获取数组的长度
int count=array.length;
//第一个循环决定比较间隔
for(int i=count/2;i>0;i=i/2){
//第二个循环根据间隔将数组分为若干的数组
for(int j=i;j<count;j++){
//第三个循环,再子数组插入排序算法
for(int k=j;k>=i && array[k]<array[k-i];k=k-i){
int temp=array[k];
array[k]=array[k-i];
array[k-i]=temp;
}
}
}
printArray(array);
}
简单选择排序:在一组数据中选出最小的数和第一个交换,然后在选出最小的数和第二个进行交换
//实现简单选择排序
public void simpleness(int[] array){
int count=0;
for(int i=0;i<array.length;i++){//遍历所有的数据
count=i;//需要遍历的游标值
int temp=array[i];//游标值对应的数值
for(int j=i+1;j<array.length;j++){//遍历最小值
if(array[j]<temp){//如果temp比下面数据大就交换
temp=array[j];
count=j;
}
}
//将数据的最小值和第一个数进行交换
array[count]=array[i];
array[i]=temp;
}
printArray(array);
}
堆排序:最麻烦的…堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法,它是选择排序的一种。可以利用数组的特点快速定位指定索引的元素。堆分为大顶堆和小顶堆,是完全二叉树,二叉树就是一个根有两个节点或一个节点,并且根节点的值一定比支点的值大,简单理解就是不断建堆,交换元素最后完成排序。为了能简单理解直接看图吧(图片源于百度)。
//实现堆排序
public void heapsort(int[] array){
//取下标
int arrayLength=array.length;
//循环建堆
//循环建堆
for (int i = 0; i < arrayLength - 1; i++) {
//建堆
buildMaxHeap(array, arrayLength - 1 - i);
//交换堆顶和最后一个元素
swap(array, 0, arrayLength - 1 - i);
//建堆并交换后的数据
}
//输出排列好的数组
printArray(array);
}
private void swap(int[] data, int i, int j) {
int tmp = data[i];
data[i] = data[j];
data[j] = tmp;
}
//对data数组从0到lastIndex建大顶堆
private void buildMaxHeap(int[] data, int lastIndex) {
//从lastIndex处节点(最后一个节点)的父节点开始
for (int i = (lastIndex - 1) / 2; i >= 0; i--) {
//k保存正在判断的节点
int k = i;
//如果当前k节点的子节点存在
while (k * 2 + 1 <= lastIndex) {
//k节点的左子节点的索引
int biggerIndex = 2 * k + 1;
//如果biggerIndex小于lastIndex,即biggerIndex+1代表的k节点的右子节点存在
if (biggerIndex < lastIndex) {
//若果右子节点的值较大
if (data[biggerIndex] < data[biggerIndex + 1]) {
//biggerIndex总是记录较大子节点的索引
biggerIndex++;
}
}
//如果k节点的值小于其较大的子节点的值
if (data[k] < data[biggerIndex]) {
//交换他们
swap(data, k, biggerIndex);
//将biggerIndex赋予k,开始while循环的下一次循环,重新保证k节点的值大于其左右子节点的值
k = biggerIndex;
} else {
break;
}
}
}
}
冒泡排序:将二个相邻的数进行比较和互换,大的数下沉,小的数上浮。
//实现冒泡排序
public void bubbling(int[] array){
int count = 0;
for (int i = 1; i < array.length; i++) {
for (int j = 1; j < array.length - i; j++) {
if (array[j] > array[j + 1]) {
count = array[j];
array[j] = array[j + 1];
array[j + 1] = count;
}
}
}
//输出排列好的数组
printArray(array);
}
快速排序:冒泡排序的一种,随机选中一个值放到一个合适的位置(感觉太抽象了),就比如说选择数组的第一个指,先从右往左跑找到一个小于第一个数的,在从左向右跑找到一个比第一个数大的进行交换,重复完成排序
//调用
test.quickSort(array,0,array.length-1);
//实现快速排序
public void quickSort(int[] array,int low,int high){
int i;
int j;
int temp;
if(low>high){
return;
}
i=low;
j=high;
temp=array[low];//以左边位基准
while(i<j){
//先看右边,依次往左边替增
while (temp<=array[j]&&i<j){
j--;
}
//再看左边,依次往右递增
while (temp>=array[i]&&i<j) {
i++;
}
//如果满足条件则交换
if (i<j) {
int z = array[i];
int y = array[j];
array[i] = y;
array[j] = z;
}
//最后将基准为与i和j相等位置的数字交换
array[low] = array[i];
array[i] = temp;
//递归调用左半数组
quickSort(array, low, j-1);
//递归调用右半数组
quickSort(array, j+1, high);
}
printArray(array);
}
归并排序:和希尔排序基本属性同理,利用递归主要是拆分以及合并这二步,基本上可以理解将一组数据拆分成二组,然后再进行拆分,直到变成单个数据再进行比较合并。
//实现归并排序
test.merging(array,0,array.length-1);
System.out.println(Arrays.toString(array));
//参考了下别人的写法
public void merging(int[] array,int low,int high){
int count = (low + high) / 2;
if (low >= high) {//分到最后一个元素时退出递归
return;
}
merging(array, low, count);
merging(array, count + 1, high);
merge(array, low, count, high);
}
private static void merge(int[] ans, int left, int mid, int right) {
//声明三个计时器
int Actr = left;
int Bctr = mid + 1;
int Cctr = 0;
int lenA = mid - left + 1;
int lenB = right - mid;
//创建临时数组,长度为A,B数组长度之和
int[] tmp = new int[right - left + 1];
//循环A,B中长度较短的长度次数的二倍的次数
while (Actr <= mid && Bctr <= right) {
if (ans[Actr] <= ans[Bctr]) {
tmp[Cctr++] = ans[Actr];
Actr++;
} else {
tmp[Cctr++] = ans[Bctr];
Bctr++;
}
}
//如果左边的还有剩余,将左边剩余的归并
while (Actr <= mid){
tmp[Cctr ++] = ans[Actr ++];
}
//如果右边的还有剩余,将右边剩余的归并
while (Bctr <= right){
tmp[Cctr ++] = ans[Bctr ++];
}
//将临时数组更新到原数组
for (int i = 0; i < tmp.length; i++) {
ans[left++] = tmp[i];
}
}
基数排序:基数排序属于“分配式排序”,它通过元素的各个位的值,将元素放置对应的“桶”中,就相当于分配一个0-9的桶,将数组的首位数字进行一一对应,然后按顺序取出来,如果一组数中有个位、十位、百位,就一一去进行比较,到十位时候将其个位按之前的放入顺序放入0位,继续匹配。
//实现基数排序
public void radixSort(int[] array){
//首先确定排序的趟数;
int max = array[0];
for (int i = 1; i < array.length; i++) {
if (array[i] > max) {
max = array[i];
}
}
int count = 0;
//判断位数;
while (max > 0) {
max /= 10;
count++;
}
//建立10个桶;
List<ArrayList> queue = new ArrayList<ArrayList>();
for (int i = 0; i < 10; i++) {
ArrayList<Integer> queue1 = new ArrayList<Integer>();
queue.add(queue1);
}
//进行分配和收集;
for (int i = 0; i < count; i++) {
//分配数组元素;
for (int j = 0; j < array.length; j++) {
//得到数字的第time+1位数;
int x = array[j] % (int) Math.pow(10, i + 1) / (int) Math.pow(10, i);
ArrayList<Integer> queue2 = queue.get(x);
queue2.add(array[j]);
queue.set(x, queue2);
}
int temp = 0;//元素计数器;
//收集队列元素;
for (int k = 0; k < 10; k++) {
while (queue.get(k).size() > 0) {
ArrayList<Integer> queue3 = queue.get(k);
array[temp] = queue3.get(0);
queue3.remove(0);
temp++;
}
}
}
printArray(array);
}
稀疏数组
概念:当一个数组中大部分元素为0,或者为同一值的数组时,可以使用稀疏数组来保存该数组。稀疏数组的处理方式是:记录数组一共有几行几列,有多少个不同值;把具有不同值的元素和行列及值记录在一个小规模的数组中,从而缩小程序的规模
package cn.com.ly.main;
/**
* @author 三日间的幸福
* @version 1.0
* @remark
* @create 2021-05-18 15:29
*/
public class Test {
public static void main(String[] args) {
//创建一个二维数组 11*11 0:没有棋子, 1:黑棋, 2:白棋
int [][] a = new int[8][8];
a[1][2] = 1;
a[2][3] = 2;
System.out.println("原数组为:");
for (int[] x : a) {
for (int i : x) {
System.out.print(i + "\t");
}
System.out.println();
}
System.out.println("=============================");
//转化为稀疏数组
//先获取有效值的个数
int sum = 0;
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 8; j++) {
if (a[i][j] != 0){
sum ++;
}
}
}
System.out.println("有效值的个数为:" + sum);
//创建稀疏数组
int[][] b = new int[sum+1][3];
b[0][0] = 8;
b[0][1] = 8;
b[0][2] = sum;
//遍历二维数组,将非0的值存放到稀疏数组
int count = 0;
for (int i = 0; i < a.length; i++) {
for (int j = 0; j < a[i].length; j++) {
if (a[i][j] != 0){
count++;
b[count][0] = i;
b[count][1] = j;
b[count][2] = a[i][j];
}
}
}
System.out.println("稀疏数组为:");
for (int i = 0; i < b.length; i++) {
System.out.println(b[i][0] + "\t" + b[i][1] + "\t" + b[i][2] + "\t");
}
System.out.println("=============================");
System.out.println("还原数组:");
//读取稀疏数组
int[][] c = new int[b[0][0]][b[0][1]];
//还原数组的值
for (int i = 1; i <b.length ; i++) {
c[b[i][0]][b[i][1]] = b[i][2];
}
System.out.println("还原数组为:");
for (int[] i: c) {
for (int j:i) {
System.out.print(j + "\t");
}
System.out.println();
}
}
}
展示:
总结
概念:到这里java基础复习总结就差不多了,基本就是按照b站的狂神说java所总结的,下一篇文章应该是面向对象复习(话说面向对象不就是java基础吗?好吧就是我懒),如果说上面的总结还有我没讲到的点,请各位大佬再评论区中指出,最后很感谢你能看到这里,谢谢!
狂神官网地址:点这里o( ̄▽ ̄)o