Flutter初体验(二)---- 创建第一个Flutter APP

在第一篇文章 Flutter初体验(一)—Mac 安装配置,学习了配置 Flutter 开发环境,并运行了Demo项目,本篇根据官方教程,学习创建自己的第一个Flutter APP。


参考文档 https://flutter.io/get-started/codelab/

项目需求

您将实施一个简单的移动应用程序,为一家创业公司生成建议名称。用户可以选择和取消选择名称,保存最好的名称。该代码一次生成十个名称。当用户滚动时,会生成新批次的名称。用户可以点击应用栏右上方的列表图标以移动到仅列出收藏名称的新路线

  • 下面的GIF动画显示我们将要完成应用程序

学习成果:
  • Flutter应用程序的基本结构
  • 学习查找和使用包来扩展功能
  • 使用热重载加快开发周期
  • 如何实现状态化组件
  • 如何创建一个无限的,延迟加载的列表
  • 如何创建并导航到第二个屏幕
  • 如何使用主题更改应用程序的外观

第1步 :创建一个运行的Flutter 应用程序

在第一篇文章中已经讲述了如何配置Flutter开发环境,并且创建一个示例Flutter项目,现在将会修改这个示例程序来完成我们的项目。

本次操作,主要编辑Dart代码中的lib/main.dart

提示: 将代码粘贴到您的应用中时,缩进可能会变形。您可以使用Flutter工具自动修复此问题

  • Android Studio / IntelliJ IDEA:右键单击Dart代码,然后选择使用dartfmt重新格式化代码。
  • VS代码:右键单击并选择格式化文档
  • Terminal: 运行 flutter format

1.替换 lib/main.dart.
删除lib / main.dart中的所有代码。替换为下面的代码,它在屏幕的中心显示“Hello World”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Welcome to Flutter',
home: new Scaffold(
appBar: new AppBar(
title: new Text('Welcome to Flutter'),
),
body: new Center(
child: new Text('Hello World'),
),
),
);
}
}

2.运行工程,将会看到下面的屏幕截图

总结

  • 本示例创建一个Material应用程序。 Material是一种在移动设备和web端是统一标准的视觉设计语言。 Flutter提供了一套丰富的Material小部件
  • 主要方法指定箭头(=>)表示法,它是用于单行函数或方法的简写
  • 该应用程序扩展了使应用程序本身成为小部件的StatelessWidget。在Flutter中,几乎所有东西都是一个小部件,包括对齐,填充和布局
  • 小部件的主要工作是提供一个build()方法,该方法描述如何根据其他较低级别的小部件显示小部件
  • 此示例的小部件树由包含Text小部件的中心小部件组成。中心小部件将其小部件子树对齐到屏幕中心


    第2步:使用外部包

    在这一步中,您将开始使用名为english_words的开源软件包,其中包含数千个最常用的英文单词以及一些实用功能
    1.pubspec文件管理Flutter应用程序的资源。在pubspec.yaml中,将english_words(3.1.0或更高版本)添加到依赖项列表。新行强调如下:

    1
    2
    3
    4
    5
    6
    dependencies:
    flutter:
    sdk: flutter
    cupertino_icons: ^0.1.0
    english_words: ^3.1.0

    2.在Android Studio的编辑器视图,单击右上角Packages get。将该包引入您的项目。将会在控制台中看到以下内容

    1
    2
    3
    flutter packages get
    Running "flutter packages get" in flutter_app...
    Process finished with exit code 0

    3.在lib / main.dart中,将english_words导入,如下所示:

    1
    2
    import 'package:flutter/material.dart';
    import 'package:english_words/english_words.dart';

    在输入过程中,Android Studio会为您提供有关库导入的建议。然后它将呈现灰色的导入字符串,让您知道导入的库未使用(到目前为止)

    4.使用English words 包来生成文本取代字符串“Hello World”
    进行以下更改:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    import 'package:flutter/material.dart';
    import 'package:english_words/english_words.dart';
    void main() => runApp(new MyApp());
    class MyApp extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
    final wordPair = new WordPair.random();
    return new MaterialApp(
    title: 'Welcome to Flutter',
    home: new Scaffold(
    appBar: new AppBar(
    title: new Text('Welcome to Flutter'),
    ),
    body: new Center(
    //child: new Text('Hello World'), // Replace the highlighted text...
    child: new Text(wordPair.asPascalCase), // With this highlighted text.
    ),
    ),
    );
    }
    }

    5.如果应用程序正在运行,使用热重载按钮(⚡️)更新正在运行的应用程序,每次点击热重载或者保存项目时,都会在正在运行的应用程序中随机显示一个不同的单词,这是单词配对是在构建方法内部生成的,每次MaterialApp需要渲染时都会运行它,或在检查器中切换平台时。


    第3步:添加一个有状态的组件

无状态组件是不可变的,意味着他们的属性不能被改变–所有的值都是最终值

有状态的小部件保持在小部件的生命周期中可能改变的状态,实现一个有状态的小部件至少需要两个类:1)一个StatefulWidget类,它创建一个取代 2)一个State类的实例。 StatefulWidget类本身是不可变的,但State类在整个构件的生命周期中保持不变

在这一步中,您将添加一个有状态的小部件RandomWords,它创建其状态类RandomWordsState
1.将有状态的RandomWords小部件添加到main.dart,它可以在MyApp之外的文件中的任何位置使用,但解决方案将它放在文件的底部。 RandomWords小部件除了创建其状态类之外无其他作用

1
2
3
4
class RandomWords extends StatefulWidget {
@override
createState() => new RandomWordsState();
}

2.添加RandomWordsState类。该应用程序的大部分代码都驻留在该类中,该类保持RandomWords小部件的状态,这个类将保存随着用户滚动而无限增长的生成的单词对,以及最喜欢的单词对,因为用户通过切换红心图标来将它们从列表中添加或删除。

你会一点一点地建立这个类。首先,通过添加突出显示的文本创建一个最小类:

1
2
class RandomWordsState extends State<RandomWords> {
}

3.在添加状态类后,IDE会抱怨该类缺少构建方法,接下来,您将添加一个基本构建方法,该方法通过将单词生成代码从MyApp移动到RandomWordsState来生成单词对

将构建方法添加到RandomWordState中,如突出显示的文本所示:

1
2
3
4
5
6
7
class RandomWordsState extends State<RandomWords> {
@override
Widget build(BuildContext context) {
final wordPair = new WordPair.random();
return new Text(wordPair.asPascalCase);
}
}

4.通过下面突出显示的更改从MyApp中删除单词生成代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Welcome to Flutter',
home: new Scaffold(
appBar: new AppBar(
title: new Text('Welcome to Flutter'),
),
body: new Center(
//child: new Text(wordPair.asPascalCase), // Change the highlighted text to...
child: new RandomWords(), // ... this highlighted text
),
),
);
}
}

重新启动应用程序。如果您尝试重新加载热点,则可能会看到警告:

1
2
3
Reloading...
Not all changed program elements ran during view reassembly; consider
restarting.

这可能是误报,但考虑重新启动以确保您的更改反映在应用的用户界面中。

应用程序应该像以前一样运行,每次热重新加载或保存应用程序时都会显示一个字对。


第4步:创建一个无限的滚动ListView

在这一步中,您将展开RandomWordsState以生成并显示单词配对列表。当用户滚动时,ListView小部件中显示的列表将无限增长。 ListView的构建器工厂构造函数允许您根据需要懒惰地构建列表视图。

1.将一个_suggestions列表添加到RandomWordsState类以保存建议的词对。该变量以下划线(_)开头 - 在下划线前加上一个带有下划线的标识符可以强化Dart语言的隐私。

另外,添加一个largerFont变量来使字体变大。

1
2
3
4
5
6
class RandomWordsState extends State<RandomWords> {
...
final _suggestions = <WordPair>[];
final _biggerFont = const TextStyle(fontSize: 18.0);
}

2.将一个_buildSuggestions()函数添加到RandomWordsState类。此方法构建显示建议词对的ListView。

ListView类提供了一个构建器属性itemBuilder,一个指定为匿名函数的工厂构建器和回调函数,两个参数传递给函数 - BuildContext和行迭代器.迭代器从0开始,每次调用该函数时递增,每次建议的单词配对一次.该模型允许建议的列表在用户滚动时无限增长。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class RandomWordsState extends State<RandomWords> {
...
Widget _buildSuggestions() {
return new ListView.builder(
padding: const EdgeInsets.all(16.0),
// The itemBuilder callback is called once per suggested word pairing,
// and places each suggestion into a ListTile row.
// For even rows, the function adds a ListTile row for the word pairing.
// For odd rows, the function adds a Divider widget to visually
// separate the entries. Note that the divider may be difficult
// to see on smaller devices.
itemBuilder: (context, i) {
// Add a one-pixel-high divider widget before each row in theListView.
if (i.isOdd) return new Divider();
// The syntax "i ~/ 2" divides i by 2 and returns an integer result.
// For example: 1, 2, 3, 4, 5 becomes 0, 1, 1, 2, 2.
// This calculates the actual number of word pairings in the ListView,
// minus the divider widgets.
final index = i ~/ 2;
// If you've reached the end of the available word pairings...
if (index >= _suggestions.length) {
// ...then generate 10 more and add them to the suggestions list.
_suggestions.addAll(generateWordPairs().take(10));
}
return _buildRow(_suggestions[index]);
}
);
}
}

3._buildSuggestions函数每个字对调用_buildRow一次。这个函数在ListTile中显示每个新对,这允许您在下一步中使行更具吸引力

RandomWordsState添加_buildRow函数:

1
2
3
4
5
6
7
8
9
10
11
12
class RandomWordsState extends State<RandomWords> {
...
Widget _buildRow(WordPair pair) {
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
);
}
}

4.更新RandomWordsState的构建方法以使用_buildSuggestions(),而不是直接调用单词生成库。进行突出显示的更改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class RandomWordsState extends State<RandomWords> {
...
@override
Widget build(BuildContext context) {
//final wordPair = new WordPair.random(); // Delete these two lines.
//return new Text(wordPair.asPascalCase);
return new Scaffold (
appBar: new AppBar(
title: new Text('Startup Name Generator'),
),
body: _buildSuggestions(),
);
}
...
}

5.更新MyApp的构建方法。从MyApp中删除Scaffold和AppBar实例。这些将由RandomWordsState管理,当用户在下一步中从一个屏幕导航到另一个屏幕时,可以更轻松地更改应用栏中的路由名称。

用下面突出显示的构建方法替换原始方法:

1
2
3
4
5
6
7
8
9
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Startup Name Generator',
home: new RandomWords(),
);
}
}

重新启动应用程序。你应该看到一个单词配对清单。尽可能向下滚动,您将继续看到新的单词配对。


第5步:添加交互性

在这一步中,您将为每一行添加可点击的心形图标。当用户点击列表中的条目,切换其“收藏”状态时,该单词配对被添加到一组保存的收藏夹中或从中删除

1.将一个_saved集合添加到RandomWordsState。这个集合存储用户最喜欢的单词配对。 Set比List更受欢迎,因为正确实施的Set不允许重复输入。

1
2
3
4
5
6
7
8
class RandomWordsState extends State<RandomWords> {
final _suggestions = <WordPair>[];
final _saved = new Set<WordPair>(); //添加 saved 集合
final _biggerFont = const TextStyle(fontSize: 18.0);
}

2.在_buildRow函数中,添加alreadySaved检查以确保单词配对尚未添加到收藏夹.

1
2
3
4
Widget _buildRow(WordPair pair) {
final alreadySaved = _saved.contains(pair);
...
}

3.同样在_buildRow()中,将心形图标添加到ListTiles以启用收藏。稍后,您将添加与心形图标进行交互的功能。

添加下面突出显示的行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Widget _buildRow(WordPair pair) {
final alreadySaved = _saved.contains(pair);
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
//添加心形图标以添加收藏功能
trailing: new Icon(
alreadySaved ? Icons.favorite : Icons.favorite_border,
color: alreadySaved ? Colors.red : null,
),
);
}

4.重新启动应用程序。你现在应该在每一行看到开放的心,但它们还没有互动。

5.在_buildRow函数中让心形图标可点击。如果单词条目已经添加到收藏夹中,再次点击它将其从收藏夹中删除。当心形图标被轻敲时,函数调用setState()来通知框架状态已经改变。

添加下列代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Widget _buildRow(WordPair pair) {
final alreadySaved = _saved.contains(pair);
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
trailing: new Icon(
alreadySaved ? Icons.favorite : Icons.favorite_border,
color: alreadySaved ? Colors.red : null,
),
//修改心形图标的状态
onTap: () {
setState(() {
if (alreadySaved) {
_saved.remove(pair);
} else {
_saved.add(pair);
}
});
},
);
}

热重载应用程序。你应该能够点击任何一行以获得最喜欢的,或不适合的条目。注意,点击一行会生成从心形图标散发的涟漪动画。


第6步:导航到新的屏幕

在这一步中,您将添加一个显示收藏夹的新屏幕(在Flutter中称为路线)。您将学习如何在主路线和新路线之间导航。

在Flutter中,导航器管理包含应用程序路线的堆栈。将路线推入导航器的堆栈,将显示更新为该推入的路线。从导航器的堆栈中弹出路径,将显示返回到上一个路线。

1.向RandomWordsState的构建方法中的AppBar添加列表图标。当用户点击列表图标时,包含收藏夹项目的新路线被推送到导航器,显示该图标。

提示:某些小部件属性采用单个小部件(子级),而其他属性(如操作)则采用小部件(子级)数组,如方括号([])所示。

将该图标及其相应的操作添加到构建方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class RandomWordsState extends State<RandomWords> {
...
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Startup Name Generator'),
//添加actions
actions: <Widget>[
new IconButton(icon: new Icon(Icons.list), onPressed: _pushSaved),
],
),
body: _buildSuggestions(),
);
}
...
}

2.将一个_pushSaved()函数添加到RandomWordsState类。

1
2
3
4
5
class RandomWordsState extends State<RandomWords> {
...
void _pushSaved() {
}
}

热重新加载应用程序。列表图标出现在应用程序栏中。点击它什么也没做,因为_pushSaved函数是空的。

3.当用户点击应用栏中的列表图标时,建立一条路线并将其推送到导航器的堆栈。此操作会更改屏幕以显示新路线。

新页面的内容是使用匿名函数在MaterialPageRoute的构建器属性中构建的。

将呼叫添加到Navigator.push,如突出显示的代码所示,将路线推送到导航器的堆栈。

1
2
3
4
void _pushSaved() {
Navigator.of(context).push(
);
}

4.添加MaterialPageRoute及其构建器。现在,添加生成ListTile行的代码。ListTile的divideTiles()方法在每个ListTile之间添加水平间距。divided的变量保存最后的行,通过便利函数toList()转换为列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void _pushSaved() {
Navigator.of(context).push(
new MaterialPageRoute(
builder: (context) {
final tiles = _saved.map(
(pair) {
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
);
},
);
final divided = ListTile
.divideTiles(
context: context,
tiles: tiles,
)
.toList();
},
),
);
}

5.构建器属性返回一个Scaffold,其中包含名为“Saved Suggestions”的新路线的应用程序栏。新路由的主体由包含ListTiles行的ListView组成;每行由一个分隔符分隔。

添加下面突出显示的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void _pushSaved() {
Navigator.of(context).push(
new MaterialPageRoute(
builder: (context) {
final tiles = _saved.map(
(pair) {
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
);
},
);
final divided = ListTile
.divideTiles(
context: context,
tiles: tiles,
)
.toList();
//添加 Scaffold 名为 Saved Suggestions
return new Scaffold(
appBar: new AppBar(
title: new Text('Saved Suggestions'),
),
body: new ListView(children: divided),
);
},
),
);
}

6.热重载应用程序。最喜欢的一些选择,并点击应用栏中的列表图标。新路线显示包含收藏夹。请注意,导航器会在应用栏中添加一个“返回”按钮。你不必显式实现Navigator.pop。点击后退按钮返回到主页路线。


第7步:使用主题更改UI

在最后一步中,您将使用该应用的主题,主题控制你的应用的外观和感觉。您可以使用默认主题,该主题取决于物理设备或模拟器,也可以自定义主题以反映您的品牌。

1.您可以通过配置ThemeData类轻松更改应用程序的主题。您的应用程序目前使用默认主题,但您将更改主要颜色为白色。

将突出显示的代码添加到MyApp,将应用程序的主题更改为蓝色:

1
2
3
4
5
6
7
8
9
10
11
12
13
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Startup Name Generator',
//修改主题颜色
theme: new ThemeData(
primaryColor: Colors.blue,
),
home: new RandomWords(),
);
}
}

2.热重新加载应用程序。请注意,整个背景是白色的,甚至是应用栏。

3.作为读者的练习,使用ThemeData来改变用户界面的其他方面。材质库中的Colors类提供了许多可以使用的颜色常量,而热重载使得用户界面的试验变得快速而简单。


搞定!

我们编写了一个在iOS和Android上运行的交互式Flutter应用程序。在这个codelab中,你有:

  • 从头开始创建一个Flutter应用程序。
  • 书写Dart代码。
  • 利用外部的第三方库。
  • 使用热重载加快开发周期。
  • 实现一个有状态的小部件,为你的应用增加交互性。
  • 用ListView和ListTiles创建一个延迟加载的无限滚动列表。
  • 创建了一条路线并添加了在主路线和新路线之间移动的逻辑。
  • 了解如何使用主题更改应用UI的外观。

第一篇文章: Flutter初体验(一)—Mac 安装配置

Flutter初体验(一)---- 配置安装Flutter


参考文档:
https://flutter.io/setup-macos/ http://blog.csdn.net/zhangxiangliang2/article/details/75566412


1. 获取 Flutter SDK

  • clone Flutter

    1、使用git将Flutter项目克隆到本地,然后将Flutter添加到本地环境变量,
    1
    git clone -b beta https://github.com/flutter/flutter.git

2、到Flutter保存的路径下

1
export PATH=`pwd`/flutter/bin:$PATH

2.1、如果下载失败,Google为中国地区提供了镜像

1
2
3
4
5
6
export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
git clone -b dev https://github.com/flutter/flutter.git
export PATH="$PWD/flutter/bin:$PATH"
cd ./flutter
flutter doctor

  • Run flutter doctor

    运行Flutter doctor命令检查是否需要安装依赖项,完成安装
    1
    flutter doctor

这个命令会检查环境并在窗口显示报告,Dart SDK与Flutter捆绑在一起;没有必要单独安装Dart。仔细检查输出是否需要安装其他软件或执行其他任务

如果不出意外,报告如下:

1
2
3
4
5
6
7
8
9
10
11
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel dev, v0.1.7, on Mac OS X 10.12.6 16G1212, locale zh-Hans-CN)
[✓] Android toolchain - develop for Android devices (Android SDK 27.0.0)
[!] iOS toolchain - develop for iOS devices (Xcode 9.2)
✗ libimobiledevice and ideviceinstaller are not installed. To install, run:
brew install --HEAD libimobiledevice
brew install ideviceinstaller
✗ ios-deploy not installed. To install:
brew install ios-deploy
[✓] Android Studio (version 3.0)
[✓] Connected devices (1 available)

第一次运行命令时,它会下载自身的依赖关系并自行编译,后续运行会快很多。如果过程失败,多重复几次,并根据命令行提示进行安装。

接下来会介绍如何执行这些任务并完成设置过程,如果你选择使用某个IDE,可用于IntelliJ IDEA, Android Studio, 和 VS Code,你会看到flutter doctor输出过程。

一旦你安装了任何缺失的依赖关系,再次运行flutter doctor命令来验证你已经正确设置了所有东西.

2. 获取文件路径

Flutter SDK下载后的路径,获取Flutter SDK 的安装路径:
/Users/用户名/flutter,要注意的是flutter文件夹下面有多个同名的flutter文件夹,真正的SDK路径只到顶层flutter文件夹
具体步骤参考网友做的GIF

打开文件管理
打开文件管理

进入sdk目录

获取文件完整路径

3. 安装Flutter插件和Dart插件

这里以AndroidStudio为例,打开Preferences面板,在Plugins中搜索Flutter,install,安装时自动安装Dart,安装完成后重启AndroidStudio

重启AndroidStudio后,File->New 中出现 New Flutter Project,说明安装成功屏幕快照 2018-03-13 20.38.38.png

4. 创建Flutter Project

  • File -> New -> New Flutter Project,选择Flutter Application -> Next,这里注意 Flutter SDK的路径是否正确

  • Next -> 设置 Company domain,Finish,Project创建完成,等待几分钟,Project完成初始化,新建Project提供了默认界面,直接运行,看看运行界面:
    image.png

至此,Flutter环境配置完成,示例也能运行,后续继续学习Flutter的开发。

Java进阶 ——— 面试常见问题

1. java中==和equals和hashCode的区别

  • 1.关系操作符“==”到底比较的是什么?

“关系操作符生成的是一个boolean结果,它们计算的是操作数的值之间的关系”。

  这句话看似简单,理解起来还是需要细细体会的。说的简单点,==就是用来比较值是否相等
在Java中游8种基本数据类型:

  浮点型:float(4 byte), double(8 byte)

  整型:byte(1 byte), short(2 byte), int(4 byte) , long(8 byte)

  字符型: char(2 byte)

  布尔型: boolean(JVM规范没有明确规定其所占的空间大小,仅规定其只能够取字面值”true”和”false”)

  对于这8种基本数据类型的变量,变量直接存储的是“值”,因此在用关系操作符==来进行比较时,比较的就是 “值” 本身。要注意浮点型和整型都是有符号类型的,而char是无符号类型的(char类型取值范围为0~2^16-1).

  也就是说比如:

  int n=3;

  int m=3; 

  变量n和变量m都是直接存储的”3”这个数值,所以用==比较的时候结果是true。

  • 2.equals比较的又是什么?

equals方法是基类Object中的方法,因此对于所有的继承于Object的类都会有该方法。为了更直观地理解equals方法的作用,直接看Object类中equals方法的实现。

下面是String类中equals方法的具体实现:
String中equals

 可以看出,String类对equals方法进行了重写,用来比较指向的字符串对象所存储的字符串是否相等。

  其他的一些类诸如Double,Date,Integer等,都对equals方法进行了重写用来比较指向的对象所存储的内容是否相等。

  • 3.关于hashCode()

这个方法返回对象的散列码,返回值是int类型的散列码。
对象的散列码是为了更好的支持基于哈希机制的Java集合类,例如 Hashtable, HashMap, HashSet 等。

关于hashCode方法,一致的约定是:
重写了euqls方法的对象必须同时重写hashCode()方法。

如果2个对象通过equals调用后返回是true,那么这个2个对象的hashCode方法也必须返回同样的int型散列码

如果2个对象通过equals返回false,他们的hashCode返回的值允许相同。(然而,程序员必须意识到,hashCode返回独一无二的散列码,会让存储这个对象的hashtables更好地工作。)

相比 于 equals公认实现约定,hashCode的公约要求是很容易理解的。有2个重点是hashCode方法必须遵守的。约定的第3点,其实就是第2点的
细化,下面我们就来看看对hashCode方法的一致约定要求。

第一:在某个运行时期间,只要对象的(字段的)变化不会影响equals方法的决策结果,那么,在这个期间,无论调用多少次hashCode,都必须返回同一个散列码。

第二:通过equals调用返回true 的2个对象的hashCode一定一样。

第三:通过equasl返回false 的2个对象的散列码不需要不同,也就是他们的hashCode方法的返回值允许出现相同的情况。

总结一句话:等价的(调用equals返回true)对象必须产生相同的散列码。不等价的对象,不要求产生的散列码不相同。


总结来说:

  1)对于==,如果作用于基本数据类型的变量,则直接比较其存储的 “值”是否相等;

    如果作用于引用类型的变量,则比较的是所指向的对象的地址

  2)对于equals方法,注意:equals方法不能作用于基本数据类型的变量

    如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址;

    诸如String、Date等类对equals方法进行了重写的话,比较的是所指向的对象的内容


作者:海子 
出处:http://www.cnblogs.com/dolphin0520/


2.int、char、long各占多少字节数

int 4字节 char 2字节 long 8字节


3. int和Integer的区别

  • 1、Integer是int的包装类,int则是java的一种基本数据类型
  • 2、Integer变量必须实例化后才能使用,而int变量不需要
  • 3、Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int则是直接存储数据值
  • 4、Integer的默认值是null,int的默认值是0

延伸:
关于Integer和int的比较
1、由于Integer变量实际上是对一个Integer对象的引用,所以两个通过new生成的Integer变量永远是不相等的(因为new生成的是两个对象,其内存地址不同)。

1
2
3
Integer i = new Integer(100);
Integer j = new Integer(100);
System.out.print(i == j); //false

2、Integer变量和int变量比较时,只要两个变量的值是向等的,则结果为true(因为包装类Integer和基本数据类型int比较时,java会自动拆包装为int,然后进行比较,实际上就变为两个int变量的比较)

1
2
3
Integer i = new Integer(100);
int j = 100;
System.out.print(i == j); //true

3、非new生成的Integer变量和new Integer()生成的变量比较时,结果为false。(因为非new生成的Integer变量指向的是java常量池中的对象,而new Integer()生成的变量指向堆中新建的对象,两者在内存中的地址不同)

1
2
3
Integer i = new Integer(100);
Integer j = 100;
System.out.print(i == j); //false

4、对于两个非new生成的Integer对象,进行比较时,如果两个变量的值在区间-128到127之间,则比较结果为true,如果两个变量的值不在此区间,则比较结果为false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Integer i = 100;
Integer j = 100;
System.out.print(i == j); //true
Integer i = 128;
Integer j = 128;
System.out.print(i == j); //false
对于第4条的原因:
java在编译Integer i = 100 ;时,会翻译成为Integer i = Integer.valueOf(100);,而java API中对Integer类型的valueOf的定义如下:
public static Integer valueOf(int i){
assert IntegerCache.high >= 127;
if (i >= IntegerCache.low && i <= IntegerCache.high){
return IntegerCache.cache[i + (-IntegerCache.low)];
}
return new Integer(i);
}

java对于-128到127之间的数,会进行缓存,Integer i = 127时,会将127进行缓存,下次再写Integer j = 127时,就会直接从缓存中取,就不会new了


4. Java多态的理解

一、 多态的概念:同一操作作用于不同对象,可以有不同的解释,有不同的执行结果,这就是多态,简单来说就是:父类的引用指向子类对象

二、 实现多态的技术称为:动态绑定(dynamic binding),是指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。

三、 多态的作用:消除类型之间的耦合关系。

四、 现实中,关于多态的例子不胜枚举。比方说按下 F1键这个动作,如果当前在 Flash 界面下弹出的就是 AS 3 的帮助文档;如果当前在 Word 下弹出的就是 Word 帮助;在Windows 下弹出的就是 Windows 帮助和支持。同一个事件发生在不同的对象上会产生不同的结果

五、 多态存在的三个必要条件

  • 一、要有继承;
  • 二、要有重写;
  • 三、父类引用指向子类对象。

六、多态的好处:

  1. 可替换性(substitutability)。多态对已存在代码具有可替换性。例如,多态对圆Circle类工作,对其他任何圆形几何体,如圆环,也同样工作。
  2. 可扩充性(extensibility)。多态对代码具有可扩充性。增加新的子类不影响已存在类的多态性、继承性,以及其他特性的运行和操作。实际上新加子类更容易获得多态功能。例如,在实现了圆锥、半圆锥以及半球体的多态基础上,很容易增添球体类的多态性。
  3. 接口性(interface-ability)。多态是超类通过方法签名,向子类提供了一个共同接口,由子类来完善或者覆盖它而实现的。如图8.3所示。图中超类Shape规定了两个实现多态的接口方法,computeArea()以及computeVolume()。子类,如Circle和Sphere为了实现多态,完善或者覆盖这两个接口方法。
  4. 灵活性(flexibility)。它在应用中体现了灵活多样的操作,提高了使用效率。
  5. 简化性(simplicity)。多态简化对应用软件的代码编写和修改过程,尤其在处理大量对象的运算和操作时,这个特点尤为突出和重要。
  6. 当超类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法,多态在继承链中对象方法的调用存在一个优先级:this.show(O)super.show(O)this.show((super)O)super.show((super)O)
    子类的对象,先在子类中查找是否有覆盖的方法,没有就去父类查找该方法,如果还没有,就在子类调用继承于父类的参数,并强转为父类的参数的方法,如果还没有,调用父类中方法,并将参数转为父类

5. Java中的String,StringBuilder,StringBuffer三者的区别

这三个类之间的区别主要是在两个方面,即运行速度和线程安全这两方面。

  • 1、首先说运行速度,或者说是执行速度,在这方面运行速度快慢为:StringBuilder > StringBuffer > String
    String最慢的原因:
    String为字符串常量,而StringBuilder和StringBuffer均为字符串变量,即String对象一旦创建之后该对象是不可更改的,但后两者的对象是变量,是可以更改的。
  • 2、 再来说线程安全
    在线程安全上,StringBuilder是线程不安全的,而StringBuffer是线程安全的
      如果一个StringBuffer对象在字符串缓冲区被多个线程使用时,StringBuffer中很多方法可以带有synchronized关键字,所以可以保证线程是安全的,但StringBuilder的方法则没有该关键字,所以不能保证线程安全,有可能会出现一些错误的操作。所以如果要进行的操作是多线程的,那么就要使用StringBuffer,但是在单线程的情况下,还是建议使用速度比较快的StringBuilder。

  • 3、总结一下
      String:适用于少量的字符串操作的情况
      StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况
      StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况


6. 什么是内部类?内部类的作用

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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成员和静态成员)。

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

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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成员
    }
    }
    }

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    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和包访问两种权限修饰。我个人是这么理解的,由于成员内部类看起来像是外部类的一个成员,所以可以像类的成员一样拥有多种权限修饰。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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修饰符的。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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
    }
    });

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

    1
    2
    3
    4
    5
    6
    7
    8
    new OnClickListener() {
    @Override
    public void onClick(View v) {
    // TODO Auto-generated method stub
    }
    }

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

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


Java内部类详解 
https://www.cnblogs.com/dolphin0520/p/3811445.html

7. 抽象类和接口区别

抽象类和接口的对比
参数 抽象类 接口
默认的方法实现 它可以有默认的方法实现 接口完全是抽象的。它根本不存在方法的实现
实现 子类使用extends关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现。 子类使用关键字implements来实现接口。它需要提供接口中所有声明的方法的实现
构造器 抽象类可以有构造器 接口不能有构造器
与正常Java类的区别 除了你不能实例化抽象类之外,它和普通Java类没有任何区别 接口是完全不同的类型
访问修饰符 抽象方法可以有public、protected和default这些修饰符 接口方法默认修饰符是public。你不可以使用其它修饰符。
main方法 抽象方法可以有main方法并且我们可以运行它 接口没有main方法,因此我们不能运行它。
多继承 抽象方法可以继承一个类和实现多个接口 接口只可以继承一个或多个其它接口
速度 它比接口速度要快 接口是稍微有点慢的,因为它需要时间去寻找在类中实现的方法。
添加新方法 如果你往抽象类中添加新的方法,你可以给它提供默认的实现。因此你不需要改变你现在的代码。 如果你往接口中添加方法,那么你必须改变实现该接口的类。
什么时候使用抽象类和接口
  • 如果你拥有一些方法并且想让它们中的一些有默认实现,那么使用抽象类吧。
  • 如果你想实现多重继承,那么你必须使用接口。由于Java不支持多继承,子类不能够继承多个类,但可以实现多个接口。因此你就可以使用接口来解决它。
  • 如果基本功能在不断改变,那么就需要使用抽象类。如果不断改变基本功能并且使用接口,那么就需要改变所有实现了该接口的类。

Java抽象类与接口的区别
http://www.importnew.com/12399.html

8. Java中为何要定义抽象类

抽象方法:由abstract修饰的方法为抽象方法,抽象方法只有方法的定义,没有方法的实现。
抽象类:一个类中如果包含抽象方法,个i类应该用abstract关键字声明为抽象类。
抽象类不可以实例化,即使一个类中没有抽象方法,也可以将其定义为抽象类,同样,该类不可以实例化。
抽象类的意义:

  • 1、为子类提供一个公共的类型;
  • 2、封装子类中重复内容(成员变量和方法);
  • 3、定义有抽象方法,子类虽然有不同的实现,但该方法的定义是一致的。

9. 抽象类和接口的使用场景

语义上的区别:
首先 类描述的是 这个东西是什么(强调所属)?包含了静态属性,静态行为 ,属性和行为。
而接口 描述的它能做什么事儿(强调行为)? 只是 静态常量属性 和 行为

abstract class的应用场合
一句话,在既需要统一的接口,又需要实例变量或缺省的方法的情况下,就可以使用它。最常见的有:

  • A. 定义了一组接口,但又不想强迫每个实现类都必须实现所有的接口。可以用abstract class定义一组方法体,甚至可以是空方法体,然后由子类选择自己所感兴趣的方法来覆盖。
  • B. 某些场合下,只靠纯粹的接口不能满足类与类之间的协调,还必需类中表示状态的变量来区别不同的关系。abstract的中介作用可以很好地满足这一点。
  • C. 规范了一组相互协调的方法,其中一些方法是共同的,与状态无关的,可以共享的,无需子类分别实现;而另一些方法却需要各个子类根据自己特定的状态来实现特定的功能
    其他情况下都是使用 接口。

应该这样说,我们再开始使用的时候就是用的接口,后来实现的子类里有些子类有共同属性,或者相同的方法实现,所以提取出来一个抽象类,作为类和接口的中介。


抽象类和接口的使用场景
http://blog.csdn.net/xiaoliuliu2050/article/details/61931498

10. 泛型通配符extends与super的区别

<? extends T>限定参数类型的上界:参数类型必须是T或T的子类型
<? super T> 限定参数类型的下界:参数类型必须是T或T的超类型
总结为:

  • 1.<? extends T> 只能用于方法返回,告诉编译器此返参的类型的最小继承边界为T,T和T的父类都能接收,但是入参类型无法确定,只能接受null的传入
  • 2.<? super T>只能用于限定方法入参,告诉编译器入参只能是T或其子类型,而返参只能用Object类接收
  • 3.? 既不能用于入参也不能用于返参

11. 父类的静态方法能否被子类重写

静态方法是针对于类而言的,而重写和继承是针对于对象实例的,不能针对于静态方法,因为静态方法在程序启动已经分配内存,所有引用该方法的对象,都指向内存中同一地址,所以子类并不会重写,即使子类定义了相同名称的静态方法,也只是分配了一块内存给子类的静态方法,没有重写的说法。

12. 进程和线程的区别

  • 1.定义
    进程:具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位.
    线程:进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.
  • 2.关系
    一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行.
    相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。

  • 3.区别
      进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
    1、简而言之,一个程序至少有一个进程,一个进程至少有一个线程.
    2、线程的划分尺度小于进程,使得多线程程序的并发性高。
    3、另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
    4、线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
    5、从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。

  • 4.优缺点
    线程和进程在使用上各有优缺点:
    线程执行开销小,但不利于资源的管理和保护;
    而进程正相反。
    同时,线程适合于在SMP机器上运行,而进程则可以跨机器迁移。


https://www.cnblogs.com/lgk8023/p/6430592.html

13. final、finally、finalize区别

  • Final
    Final用于修饰类、成员变量和成员方法。final修饰的类,不能被继承(String、StringBuilder、StringBuffer、Math,不可变类),其中所有的方法都不能被重写,所以不能同时用abstract和final修饰类(abstract修饰的类是抽象类,抽象类是用于被子类继承的,和final起相反的作用);Final修饰的方法不能被重写,但是子类可以用父类中final修饰的方法;Final修饰的成员变量是不可变的,如果成员变量是基本数据类型,初始化之后成员变量的值不能被改变,如果成员变量是引用类型,那么它只能指向初始化时指向的那个对象,不能再指向别的对象,但是对象当中的内容是允许改变的

  • Finally
    Finally通常和try catch搭配使用,保证不管有没有发生异常,资源都能够被释放(释放连接、关闭IO流)。

  • Finalize
    Finalize是object类中的一个方法,子类可以重写finalize()方法实现对资源的回收。垃圾回收只负责回收内存,并不负责资源的回收,资源回收要由程序员完成,Java虚拟机在垃圾回收之前会先调用垃圾对象的finalize方法用于使对象释放资源(如关闭连接、关闭文件),之后才进行垃圾回收,这个方法一般不会显示的调用,在垃圾回收时垃圾回收器会主动调用。


http://blog.csdn.net/beixiaozhang/article/details/52955862

14. 序列化的方式

  • Android开发中的序列化有两种方法。
      第一种是实现Serializable接口(是JavaSE本身就支持的),第二种是实现Parcelable接口(是Android特有功能,效率比实现Serializable接口高效,可用于Intent数据传递,也可以用于进程间通信(IPC))。实现Serializable接口非常简单,声明一下就可以了,而实现Parcelable接口稍微复杂一些,但效率更高,推荐用这种方法提高性能
  • 两种方式的区别
    1)在使用内存的时候,Parcelable比Serializable性能高,所以推荐使用Parcelable。
    2)Serializable在序列化的时候会产生大量的临时变量,从而引起频繁的GC。
    3)Parcelable不能使用在要将数据存储在磁盘上的情况,因为Parcelable不能很好的保证数据的持续性在外界有变化的情况下。尽管Serializable效率低点,但此时还是建议使用Serializable 。

15.静态属性和静态方法是否可以被继承?是否可以被重写?以及原因?

java中静态属性和静态方法可以被继承,但是没有被重写(overwrite)而是被隐藏.
原因:

  • 1.静态方法和属性是属于类的,调用的时候直接通过类名.方法名完成对,不需要继承机制及可以调用。如果子类里面定义了静态方法和属性,那么这时候父类的静态方法或属性称之为”隐藏”。如果你想要调用父类的静态方法和属性,直接通过父类名.方法或变量名完成,至于是否继承一说,子类是有继承静态方法和属性,但是跟实例方法和属性不太一样,存在”隐藏”的这种情况。
  • 2.多态之所以能够实现依赖于继承、接口和重写、重载(继承和重写最为关键)。有了继承和重写就可以实现父类的引用指向不同子类的对象。重写的功能是:”重写”后子类的优先级要高于父类的优先级,但是“隐藏”是没有这个优先级之分的。
  • 3.静态属性、静态方法和非静态的属性都可以被继承和隐藏而不能被重写,因此不能实现多态,不能实现父类的引用可以指向不同子类的对象。非静态方法可以被继承和重写,因此可以实现多态

16. 静态内部类使用的目的

静态内部类可以看做和外部类平级的类,静态内部类不可以访问外部类的非静态变量与成员方法,而非静态内部类,可以访问外部类成员变量和方法,但是内部不允许出现静态成员和静态方法。

创建静态内部类实例不需要绑定在外部类的实例

17. String 转换成 Integer的方式及原理

  • String 转 Integer 的方式

    1
    i = Integer.valueOf(str);
  • String 转 Integer 原理
    先来看看 在Integer 类中 Valueof方法:

    1
    2
    3
    public static Integer valueOf(String s) throws NumberFormatException {
    return Integer.valueOf(parseInt(s, 10));
    }

    分析一下parseInt(s, 10)方法,这里只摘取其中关键代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    public static int parseInt(String s, int radix)
    throws NumberFormatException
    {
    int result = 0;
    boolean negative = false;
    int i = 0, len = s.length();
    int limit = -Integer.MAX_VALUE;
    int multmin;
    int digit;
    if (len > 0) {
    char firstChar = s.charAt(0);
    if (firstChar < '0') { // Possible leading "+" or "-" //基本不用管
    if (firstChar == '-') {
    negative = true;
    limit = Integer.MIN_VALUE;
    } else if (firstChar != '+')
    throw NumberFormatException.forInputString(s);
    if (len == 1) // Cannot have lone "+" or "-"
    throw NumberFormatException.forInputString(s);
    i++; // i自增
    }
    multmin = limit / radix; // multmin = limit/10;
    while (i < len) {
    // Accumulating negatively avoids surprises near MAX_VALUE
    digit = Character.digit(s.charAt(i++),radix);
    if (digit < 0) {
    throw NumberFormatException.forInputString(s);
    }
    if (result < multmin) {
    throw NumberFormatException.forInputString(s);
    }
    result *= radix;
    if (result < limit + digit) {
    throw NumberFormatException.forInputString(s);
    }
    result -= digit;
    }
    } else {
    throw NumberFormatException.forInputString(s);
    }
    return negative ? result : -result;
    }

Java进阶 ——— HTTP概述

HTTP概述

Web浏览器、服务器和相关的Web应用程序都是通过HTTP相互通信。HTTP是现代全球因特网中使用的公共语言。

HTTP-因特网的多媒体信使


每天都有数亿JPEG图片、HTML页面、文本文件、MPEG电影、WAV音频文件、java小程序和其他资源在因特网游弋。HTTP可以从全世界的Web服务器上将这些信息迅速、便捷、可靠的传输到Web浏览器上。
HTTP使用的是可靠的数据传输协议,它能够确保数据在传输过程中不会被损坏或者产生混乱。对开发人员来说,无需担心HTTP通信会在传输过程中被破坏、复制或者产生畸变。开发人员可以专注于程序特有细节的编写,而不是考虑因特网中存在的一些缺陷和问题。

Web客户端和服务器


Web内容都是存储在Web服务器上。Web服务器使用HTTP协议,因此还可以称为HTTP服务器。HTTP客户端向服务器发出请求,服务器会在HTTP响应中回送所请求的数据。

最常见的HTTP客户端就是浏览器。浏览器向服务器请求HTTP对象,并将对象显示在屏幕上。

媒体类型


因特网有很多的数据类型,HTTP给每种要通过Web传输的对象都打上MIME类型(MIME type)的数据格式标签。最初设计MIME(Multipurpose Internet Mail Extension,多用途因特网邮件扩展)是为了解决不同电子邮件系统之家搬移报文时存在的问题。
Web服务器会为所有的HTTP对象数据附一个MIME类型。当Web浏览器从服务器取回一个对象时,会去查看相关的MIME类型,看看它是否知道应该如何处理这个对象。大多数浏览器都可以处理上百种常见对象:显示图片、解析并格式化HTML文件、播放音频文件、或者运行外部软件来处理特殊格式数据。
MIME类型是一种文本标记,表示一种主要的对象类型和一个特定的子类型,中间由一条斜杠来分隔。
例如:HTML格式的文本文档 text/html类型来标记。

URI


每个Web服务器都有一个名字,这样客户端就可以说明它们感兴趣的资源是什么。服务器资源名被统一称为统一资源标识符(Uniform Resource Identifier,URI)。URI有两种形式:分别是URL和URN。

  • URL

    统一资源定位符(URL,Uniform Resource Locator)是资源标识符最常见的形式。

大部分URL都遵循一种标准格式,这种格式包含三个部分。
1.URL的第一部分称为方案(scheme),说明访问资源所使用的协议类型。这部分通常是HTTP或者HTTPS(http://)。
2.第二部分给出了服务器的因特网地址(www.joes-hardware.com)。
3.其余部分指定了Web服务器上的某个资源(比如,/specials/saw-blade.gif)。
现在几乎所有的URI都是URL。

  • URN

URI的第二种形式就是URN(统一资源名)。URN是作为特定内容的唯一名称使用的,与目前的资源所在地无关。使用这些与>位置无关的URN,就可以将资源四处搬动。通过URN,还可以用同一个名字通过多种网络访问协议来访问资源。
URN仍然处于试验阶段,还未大范围使用。除非特殊说明,否则这里的都是用URL来指定URI。

事务


一个HTTP事务由一条请求命令和一个响应结果组成。这种通信通过名为HTTP报文(HTTP message)的格式化数据块进行。

  • 方法

HTTP支持几种几种不同的请求命令,这些命令被称为HTTP方法(HTTP method)。每条HTTP请求报文都包含一个方法。这个方法会告诉服务器要执行什么动作(获取一个Web页面、运行一个网关程序、删除一个文件等)。

常见的HTTP方法:

HTTP方法 描述
GET 从服务器向客户端发送命名资源
PUT 将来自客户端的数据存储到一个命名的资源服务器中
DELETE 从服务器中删除命名资源
POST 将客户端数据发送到一个服务器网关应用程序
HEAD 仅发送命名资源响应中的HTTP首部
  • 状态码

    每条HTTP响应报文返回时都会携带一个状态码。状态码是一个三位数字的代码,告知客户端请求是否成功,或者是否需要采取其他动作。

常见的HTTP状态码:
|HTTP状态码| 描述|
|—-|—-|
|200 |OK, 文档正确返回|
|302 |Redirect(重定向)。到其他地方去获取资源|
|404 |Not Found(没找到),无法找到这个资源|

  • Web页面可以包含多个对象

    应用程序完成一项任务时通常会发布多个HTTP事务。比如,Web浏览器会发布一系列的HTTP事务来获取并显示一个包含丰富图片的Web页面。浏览器会执行一个事务来获取描述页面布局的HTML”框架“,然后发布另外的HTTP事务来获取每个嵌入式图片、图像面板、Java小程序等。这些嵌入式资源甚至可能位于不同的服务器。因此,一个Web页面通常并不是单个资源,而是一组资源的集合。

报文


HTTP报文都是由一行行的简单字符串组成。HTTP报文都是纯文本,不是二进制代码。

从Web客户端发往Web服务器的HTTP报文称为请求报文(request message)。从服务器发往客户端的报文称为响应报文(reponse message),此外没有其他类型的HTTP报文。请求报文和响应报文格式类似。
HTTP报文包括以下三部分:

  • 起始行

报文的第一行就是起始行,在请求报文中用来说明要做些什么,在响应报文中说明出现了什么情况。

  • 首部字段

起始行后面有0个或者多个首部字段。每个字段都包含一个名字和一个值,为了便于解析,两者之间用冒号(:)分隔。首部以一个空行结束。添加一个首部字段和添加新行一样简单。

  • 主体

空行之后就是可选的报文主体了,其中包含了所有类型的数据。请求主体中包括了要发给Web服务器的数据,响应主体中装载了要返回给客户端的数据。起始行和首部都是文本形式且都是结构化的,而主体则不同,主体可以包含任意的二进制数据(图片、视频、音频、软件程序)。当然,主体还可以包含文本。

连接


  • TCP/IP

HTTP是个应用层协议。HTTP无需关心网络通信的具体细节;它把联网的细节都给了通用、可靠的因特网传输协议TCP/IP。
TCP提供了:

  • 无差错的数据传输。
  • 按序传输(数据总是按照发送的顺序到达);
  • 分段的数据流(可以在任意时刻以任意大小将数据发送出去)。
    因特网自身是基于TCP/IP的,它是全世界计算机网络常用的层次化分组交换网络协议集。TCP/IP 隐藏了各种网络和硬件的特点及弱点,使各种类型的计算机和网络都能够进行可靠地通信。
    只要建立了TCP连接,客户端和服务器之间的报文交换就不会丢失、被破坏、也不会出现接收时乱序。HTTP协议位于TCP的上层。HTTP使用TCP来传输其报文数据。
  • 连接、IP地址及端口号

在HTTP客户端向服务器发送报文之前,需要用网际协议(Internet Prococol,IP)地址和端口号在客户端和服务器之间建立一条TCP/IP连接。
建立一条TCP连接的过程与给公司办公室的某个人打电话的过程类似。首先,要拨打公司的电话号码。这样就能进入正确的机构了。其次,拨打要联系的那个人的分机号。
最初怎么获取服务器的IP地址呢?当然是通过URL。
先看几个URL:
http://207.200.83.29:80/index.html
http://www.netscape.com:80/index.html
http://www.netscape.com/index.html
第一个 URL 使用了机器的 IP 地址,207.200.83.29 以及端口.第二个 URL 没有使用数字形式的 IP 地址,它使用的是文本形式的域名,或者称为主机名(www.netscape.com) 。主机名就是 IP 地址比较人性化的别称。可以通过一
种称为域名服务(Domain Name Service,DNS)的机制方便地将主机名转换为 IP地址,这样所有问题就都解决了。
最后一个 URL 没有端口号。HTTP 的 URL 中没有端口号时,可以假设默认端口号是 80。有了 IP 地址和端口号,客户端就可以很方便地通过 TCP/IP 进行通信了。

  • 连接、IP地址及端口号

在HTTP客户端向服务器发送报文之前,需要用网际协议(Internet Prococol,IP)地址和端口号在客户端和服务器之间建立一条TCP/IP连接。
建立一条TCP连接的过程与给公司办公室的某个人打电话的过程类似。首先,要拨打公司的电话号码。这样就能进入正确的机构了。其次,拨打要联系的那个人的分机号。
最初怎么获取服务器的IP地址呢?当然是通过URL。
先看几个URL:
http://207.200.83.29:80/index.html
http://www.netscape.com:80/index.html
http://www.netscape.com/index.html
第一个 URL 使用了机器的 IP 地址,207.200.83.29 以及端口.第二个 URL 没有使用数字形式的 IP 地址,它使用的是文本形式的域名,或者称为主机名(www.netscape.com) 。主机名就是 IP 地址比较人性化的别称。可以通过一
种称为域名服务(Domain Name Service,DNS)的机制方便地将主机名转换为 IP地址,这样所有问题就都解决了。
最后一个 URL 没有端口号。HTTP 的 URL 中没有端口号时,可以假设默认端口号是 80。有了 IP 地址和端口号,客户端就可以很方便地通过 TCP/IP 进行通信了。

步骤如下:

(a) 浏览器从 URL 中解析出服务器的主机名;

(b) 浏览器将服务器的主机名转换成服务器的 IP 地址;

(c) 浏览器将端口号(如果有的话)从 URL 中解析出来;

(d) 浏览器建立一条与 Web 服务器的 TCP 连接;

(e) 浏览器向服务器发送一条 HTTP 请求报文;

(f) 服务器向浏览器回送一条 HTTP 响应报文;

(g) 关闭连接,浏览器显示文档。

  • 使用Telnet实例

    Telnet程序可以将键盘连接到某个目标TCP端口,并将此TCP端口的输出回送到显示屏上。Telnet常用于远程终端会话,但它几乎可以连接所有的TCP服务器,包括HTTP服务器。
    可以通过Telnet程序直接与Web服务器进行对话。通过Telnet可以打开一条到某台机器上某个端口的TCP连接,然后直接向端口输入一些字符。Web服务器会将Telnet程序作为一个Web客户端来处理,然后回送给TCP连接的数据会显示在屏幕上。
    实际例子:Telnet获取URL http://www.joes-hardware.com:80/tools.html 所指向的文档

Telnet 会查找主机名并打开一条连接,连接到在 www.joes-hardware.com 的端口 80上监听的 Web 服务器。这条命令之后的三行内容是 Telnet 的输出,告诉我们它已经建立了连接。
然后我们输入最基本的请求命令 GET/tools.html HTTP/1.1 ,发送一个提供了源端主机名的 Host 首部,后面跟上一个空行,请求从服务器 www.joes-hardware.com 上获取资源 tools.html。随后,服务器会以一个响应行、几个响应首部、一个空行和最后面的 HTML 文档主体来应答。
要明确的是,Telnet 可以很好地模拟 HTTP 客户端,但不能作为服务器使用。而且对 Telnet 做脚本自动化是很繁琐乏味的。如果想要更灵活的工具,可以去看看 nc(netcat) 。通过 nc 可以很方便地操纵基于 UDP 和 TCP 的流量(包括 HTTP) ,还可以为其编写脚本。更多细节参见 http://www.bgw.org/tutorials/utilities/nc.php

协议版本


目前HTTP有几个协议版本。

  • HTTP/0.9

HTTP 的 1991 原型版本称为 HTTP/0.9。这个协议有很多严重的设计缺陷,只应该用于与老客户端的交互。HTTP/0.9 只支持 GET 方法,不支持多媒体内容的MIME 类型、各种 HTTP 首部,或者版本号。HTTP/0.9 定义的初衷是为了获取简单的 HTML 对象,它很快就被 HTTP/1.0 取代了。

  • HTTP/1.0

1.0 是第一个得到广泛使用的 HTTP 版本。HTTP/1.0 添加了版本号、各种 HTTP首部、一些额外的方法,以及对多媒体对象的处理。HTTP/1.0 使得包含生动图片的 Web 页面和交互式表格成为可能,而这些页面和表格促使万维网为人们广泛地接受。这个规范从未得到良好地说明。在这个 HTTP 协议的商业演进和学术研究都在快速进行的时代,它集合了一系列的最佳实践。

  • HTTP/1.0+

在 20 世纪 90 年代中叶,很多流行的 Web 客户端和服务器都在飞快地向 HTTP中添加各种特性,以满足快速扩张且在商业上十分成功的万维网的需要。其中很多特性,包括持久的 keep-alive 连接、虚拟主机支持,以及代理连接支持都被加入到 HTTP 之中,并成为非官方的事实标准。这种非正式的 HTTP 扩展版本通常称为 HTTP/1.0+。

  • HTTP/1.1

HTTP/1.1 重点关注的是校正 HTTP 设计中的结构性缺陷,明确语义,引入重要的性能优化措施,并删除一些不好的特性。HTTP/1.1 还包含了对 20 世纪 90 年代末正在发展中的更复杂的 Web 应用程序和部署方式的支持。HTTP/1.1 是当前使用的 HTTP 版本。

  • HTTP-NG(又名 HTTP/2.0)

HTTP-NG 是 HTTP/1.1 后继结构的原型建议,它重点关注的是性能的大幅优化,以及更强大的服务逻辑远程执行框架。HTTP-NG 的研究工作终止于 1998 年,编写本书时,还没有任何要用此建议取代 HTTP/1.1 的推广计划。

Web结构组件


前面重点介绍了Web应用程序(Web客户端和Web服务器)是如何相互发送报文来实现基本事务处理的。因特网上还有一些其他的应用程序。下面一一介绍。

  • 代理

代理位于客户端和服务器之间的HTTP中间实体。接收所有客户端的 HTTP 请求,并将这些请求转发给服务器(可能会对请求进行修改之后转发) 。对用户来说,这些应用程序就是一个代理,代表用户访问服务器。

出于安全考虑,通常会将代理作为转发所有 Web 流量的可信任中间节点使用。代理还可以对请求和响应进行过滤。比如,在企业中对下载的应用程序进行病毒检测,或者对小学生屏蔽一些成人才能看的内容。

  • 缓存

    Web 缓存(Web cache)或代理缓存(proxy cache)是一种特殊的 HTTP 代理服务器,可以将经过代理传送的常用文档复制保存起来。下一个请求同一文档的客户端就可以享受缓存的私有副本所提供的服务了.

客户端从附近的缓存下载文档会比从远程 Web 服务器下载快得多。HTTP 定义了很多功能,使得缓存更加高效,并规范了文档的新鲜度和缓存内容的隐私性。

  • 网关

网关(gateway)是一种特殊的服务器,作为其他服务器的中间实体使用。通常用于将 HTTP 流量转换成其他的协议。网关接受请求时就好像自己是资源的源端服务器一样。客户端可能并不知道自己正在与一个网关进行通信。
例如,一个 HTTP/FTP 网关会通过 HTTP 请求接收对 FTP URI 的请求,但通过 FTP协议来获取文档 。得到的文档会被封装成一条 HTTP 报文,发送给客户端。第 8 章将探讨网关。

  • 隧道

隧道(tunnel)是建立起来之后,就会在两条连接之间对原始数据进行盲转发的HTTP 应用程序。HTTP 隧道通常用来在一条或多条 HTTP 连接上转发非 HTTP 数据,转发时不会窥探数据。
HTTP 隧道的一种常见用途是通过 HTTP 连接承载加密的安全套接字层(SSL,Secure Sockets Layer)流量,这样 SSL 流量就可以穿过只允许 Web 流量通过的防火墙了。如图所示,HTTP/SSL 隧道收到一条 HTTP 请求,要求建立一条到目的地址和端口的输出连接,然后在 HTTP 信道上通过隧道传输加密的 SSL 流量,这样就可以将其盲转发到目的服务器上去了。

  • Agent代理

用户 Agent 代理(或者简称为 Agent 代理)是代表用户发起 HTTP 请求的客户端程序。所有发布 Web 请求的应用程序都是 HTTP Agent 代理。到目前为止,我们只提到过一种 HTTP Agent 代理:Web 浏览器,但用户 Agent 代理还有很多其他类型。用fiddler抓包找头部信息会发现类似User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36
比如,有些自己会在 Web 上闲逛的自动用户 Agent 代理,可以在无人监视的情况下发布 HTTP 事务并获取内容。这些自动代理的名字通常都很生动,比如“网络蜘蛛”(spiders)或者“Web 机器人” (Web robots) 。网络蜘蛛会在 Web 上闲逛,搜集信息以构建有效的 Web 内容档案,比如一个搜索引擎的数据库或者为比较购物机器人生成的产品目录。

URL和资源

因特网资源

URL是浏览器寻找信息所需的资源位置。URI是一类更通用的资源标识符,URL是URI的一个子集。URI包括URL和URN。

URL语法

1
<scheme>://<user>:<password>@<host>:<port>/<path>;<params>?<query>#<frag>
  • scheme
    访问服务器以获取资源时要使用的协议 默认值 无
  • user
    一些方案访问资源时需要的用户名 默认值 匿名
  • password
    用户名后面可能要包含密码,中间由冒号分隔 默认值
  • host
    资源宿主服务器的主机名或点分IP地址 默认值 无
  • port
    资源宿主服务器正在监听的端口号,很多方案都有默认端口号(HTTP默认端口号为80) 默认值 每个方案特有
  • path
    服务器上资源的本地名,由一个”/“将其与前面的URL组件分隔开来.路径组件的语法与服务器和方案有关 默认值 无
  • params
    一些方案会用这个组件指定输入参数. 参数为名/值对. URL中可以包含多个参数字段,它们相互以及与路径的其余部分之间用分号(;)分隔 默认值 无
  • query
    一些方案会用这个组件传递参数以激活应用程序.查询组件的内容没有通用格式.用符号”?”将其与URL的其余部分分隔开来 默认值 无
  • frag
    一小片或一部分资源的名字.引用对象时,不会将frag字段传送给服务器。这个字段是在客户端内部使用的.通过字符”#”将其与URL其余部分分隔开来 默认值 无

http://www.joes-hardware.com/hammers;sale=false/index.html;graphic=ture
这个例子就有两个路径段,hammers和index.html。hammers路径段的参数是sale,值为false。index.html段有参数graphics,值为true。
http://www.joes-hardware.com/inventory-check.cgi?item=12731
问号右边的内容称为查询组件。URL的查询组件和标志网关资源的URL路径组件一起被发送给网关资源。基本上可以将网关当作访问其他应用程序的访问点。
HTTP服务器只处理整个对象,而不是对象的片段,客户端不能将片段传送给服务器,浏览器获得整个资源后,会根据片段来显示你感兴趣的内容。

TCP/IP的三次握手,四次分手

首先我们先来了解TCP报文段

重要的标志我在图中也有标记,重点了解标志位
ACK:确认序号有效
RST:重置连接
SYN:发起了一个新连接
FIN:释放一个连接

三次握手的过程(客户端我们用A表示,服务器端用B表示)

前提:A主动打开,B被动打开

  1. 在建立连接之前,B先创建TCB(传输控制块),准备接受客户进程的连接请求,处于LISTEN(监听)状态
  2. A首先创建TCB,然后向B发出连接请求,SYN置1,同时选择初始序号seq=x,进入SYN-SEND(同步已发送)状态
  3. B收到连接请求后向A发送确认,SYN置1,ACK置1,同时产生一个确认序号ack=x+1。同时随机选择初始序号seq=y,进入SYN-RCVD(同步收到)状态
  4. A收到确认连接请求后,ACK置1,确认号ack=y+1,seq=x+1,进入到ESTABLISHED(已建立连接)状态。向B发出确认连接,最后B也进入到ESTABLISHED(已建立连接)状态。

简单来说,就是

  1. 建立连接时,客户端发送SYN包(SYN=i)到服务器,并进入到SYN-SEND状态,等待服务器确认
  2. 服务器收到SYN包,必须确认客户的SYN(ack=i+1),同时自己也发送一个SYN包(SYN=k),即SYN+ACK包,此时服务器进入SYN-RECV状态
  3. 客户端收到服务器的SYN+ACK包,向服务器发送确认报ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手

在此穿插一个知识点就是SYN攻击,那么什么是SYN攻击?发生的条件是什么?怎么避免?
在三次握手过程中,Server发送SYN-ACK之后,收到Client的ACK之前的TCP连接称为半连接(half-open connect),此时Server处于SYN_RCVD状态,当收到ACK后,Server转入ESTABLISHED状态。SYN攻击就是 Client在短时间内伪造大量不存在的IP地址,并向Server不断地发送SYN包,Server回复确认包,并等待Client的确认,由于源地址 是不存在的,因此,Server需要不断重发直至超时,这些伪造的SYN包将产时间占用未连接队列,导致正常的SYN请求因为队列满而被丢弃,从而引起网 络堵塞甚至系统瘫痪。SYN攻击时一种典型的DDOS攻击,检测SYN攻击的方式非常简单,即当Server上有大量半连接状态且源IP地址是随机的,则可以断定遭到SYN攻击了,使用如下命令可以让之现行:

#netstat -nap | grep SYN_RECV

四次分手的过程(客户端我们用A表示,服务器端用B表示)

由于TCP连接时是全双工的,因此每个方向都必须单独进行关闭。这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的链接。收到一个FIN只是意味着这一方向上没有数据流动,既不会在收到数据,但是在这个TCP连接上仍然能够发送数据,知道这一方向也发送了FIN,首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭。

前提:A主动关闭,B被动关闭

有人可能会问,为什么连接的时候是三次握手,而断开连接的时候需要四次挥手?
这是因为服务端在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。而关闭连接时,当收到对方的FIN 报文时,仅仅表示对方不再发送数据了但是还能接收数据,己方也未必全部数据都发送给对方了,所以己方可以立即close,也可以发送一些数据给对方后,再 发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送。

  1. A发送一个FIN,用来关闭A到B的数据传送,A进入FIN_WAIT_1状态。
  2. B收到FIN后,发送一个ACK给A,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),B进入CLOSE_WAIT状态。
  3. B发送一个FIN,用来关闭B到A的数据传送,B进入LAST_ACK状态。
  4. A收到FIN后,A进入TIME_WAIT状态,接着发送一个ACK给B,确认序号为收到序号+1,B进入CLOSED状态,完成四次挥手。

简单来说就是

  1. 客户端A发送一个FIN,用来关闭客户A到服务器B的数据传送(报文段4)。
  2. 服务器B收到这个FIN,它发回一个ACK,确认序号为收到的序号加1(报文段5)。和SYN一样,一个FIN将占用一个序号。
  3. 服务器B关闭与客户端A的连接,发送一个FIN给客户端A(报文段6)。
  4. 客户端A发回ACK报文确认,并将确认序号设置为收到序号加1(报文段7)。

A在进入到TIME-WAIT状态后,并不会马上释放TCP,必须经过时间等待计时器设置的时间2MSL(最长报文段寿命),A才进入到CLOSED状态。为什么?
为了保证A发送的最后一个ACK报文段能够到达B
防止“已失效的连接请求报文段”出现在本连接中

OK~是不是很难懂的感觉?那我们来说的“人性化点的”吧

三次握手流程

  1. 客户端发个请求“开门呐,我要进来”给服务器
  2. 服务器发个“进来吧,我去给你开门”给客户端
  3. 客户端有很客气的发个“谢谢,我要进来了”给服务器

四次挥手流程

  1. 客户端发个“时间不早了,我要走了”给服务器,等服务器起身送他
  2. 服务器听到了,发个“我知道了,那我送你出门吧”给客户端,等客户端走
  3. 服务器把门关上后,发个“我关门了”给客户端,然后等客户端走(尼玛~矫情啊)
  4. 客户端发个“我知道了,我走了”,之后自己就走了

参考
http://www.cnblogs.com/qcssmd/p/5508150.html
https://blog.csdn.net/u011318165/article/details/48102939

Java设计模式 ------ 代理模式

代理模式是常用的Java设计模式,它的特征是代理类与委托类有同样的接口,代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等。代理类与委托类之间通常会存在关联关系,一个代理类的对象与一个委托类的对象关联,代理类的对象本身并不真正实现服务,而是通过调用委托类的对象的相关方法,来提供特定的服务。按照代理类的创建时期,代理类可分为两种。

静态代理类:由程序员创建或由特定工具自动生成源代码,再对其编译。在程序运行前,代理类的.class文件就已经存在了。实现类和代理类实现同一个接口,将实现类对象传递给代理类,代理类的实现方法实际是由实现类完成操作的。

动态代理类:在程序运行时,运用反射机制动态创建而成。

静态代理通常只代理一个类,动态代理是代理一个接口下的多个实现类。
静态代理事先知道要代理的是什么,而动态代理不知道要代理什么东西,只有在运行时才知道。

动态代理是实现JDK里的InvocationHandler接口的invoke方法,但注意的是代理的是接口,也就是你的业务类必须要实现接口,通过Proxy里的newProxyInstance得到代理对象。

还有一种动态代理CGLIB,代理的是类,不需要业务类继承接口,通过派生的子类来实现代理。通过在运行时,动态修改字节码达到修改类的目的。
静态代理Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public interface Iuser {
  void eat(String s);
}
public class UserImpl implements Iuser {
  @Override
  public void eat(String s) {
    System.out.println("我要吃"+s);
  }
}
public class DynamicProxy implements InvocationHandler {
  private Object object;//用于接收具体实现类的实例对象
  //使用带参数的构造器来传递具体实现类的对象
  public DynamicProxy(Object obj){
    this.object = obj;
  }
  @Override
  public Object invoke(Object proxy, Method method, Object[] args)throws Throwable {
    System.out.println("前置内容");
    method.invoke(object, args);
    System.out.println("后置内容");
    return null;
  }
}
public class ProxyTest {
  public static void main(String[] args) {
    Iuser user = new UserImpl();
    InvocationHandler h = new DynamicProxy(user);
    Iuser proxy = (Iuser) Proxy.newProxyInstance(Iuser.class.getClassLoader(), new Class[]{Iuser.class}, h);
    proxy.eat("苹果");
  }
}

动态代理Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public interface Iuser {
  void eat(String s);
}
public class UserImpl implements Iuser {
  @Override
  public void eat(String s) {
    System.out.println("我要吃"+s);
  }
}
public class DynamicProxy implements InvocationHandler {
  private Object object;//用于接收具体实现类的实例对象
  //使用带参数的构造器来传递具体实现类的对象
  public DynamicProxy(Object obj){
    this.object = obj;
  }
  @Override
  public Object invoke(Object proxy, Method method, Object[] args)throws Throwable {
    System.out.println("前置内容");
    method.invoke(object, args);
    System.out.println("后置内容");
    return null;
  }
}
public class ProxyTest {
  public static void main(String[] args) {
    Iuser user = new UserImpl();
    InvocationHandler h = new DynamicProxy(user);
    Iuser proxy = (Iuser) Proxy.newProxyInstance(Iuser.class.getClassLoader(), new Class[]{Iuser.class}, h);
    proxy.eat("苹果");
  }
}

Java 进阶 ------ 垃圾回收机制

1. 垃圾回收机制

  • 垃圾回收的意义
    当没有对象引用指向原先分配给某个对象的内存时,该内存便成为垃圾,JVM的一个系统级线程会自动释放该内存块。垃圾回收意味着程序不再需要的对象是”无用信息”,这些信息将被丢弃。当一个对象不再被引用的时候,内存回收它占领的空间,以便空间被后来的新对象使用。事实上,除了释放没用的对象,垃圾回收也可以清除内存记录碎片。

  • 垃圾收集的算法分析
    1.为什么会有年轻代

    我们先来屡屡,为什么需要把堆分代?不分代不能完成他所做的事情么?其实不分代完全可以,分代的唯一理由就是优化GC性能。你先想想,如果没有分代,那我们所有的对象都在一块,GC的时候我们要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而我们的很多对象都是朝生夕死的,如果分代的话,我们把新创建的对象放到某一地方,当GC的时候先把这块存“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。

    2.年轻代中的GC

    HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1,为啥默认会是这个比例,接下来我们会聊到。一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。

    因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。

    在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。

    3.一个对象的这一辈子
    我是一个普通的Java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor区,我就开始漂了,有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所。直到我18岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了20年(每次GC加一岁),然后被回收。


JVM 新生代老年代
https://www.cnblogs.com/E-star/p/5556188.html

2. 常见编码方式

  • 1.ASCII
    共有128个,用一个字节的低7位表示
    0~31 控制字符 如换行、回车、删除
    32~126 打印字符
  • 2.GB2312
  • 3.GBK
  • 4.UTF-16
  • 5.UTF-8

3. 静态代理和动态代理的区别,以及使用场景

原因是采用代理模式可以有效的将具体的实现与调用方进行解耦,通过面向接口进行编码完全将具体的实现隐藏在内部

  • 1.静态代理
    静态代理是在编译时就将接口、实现类、代理类一股脑儿全部手动完成,但如果我们需要很多的代理,每一个都这么手动的去创建实属浪费时间,而且会有大量的重复代码,此时我们就可以采用动态代理,动态代理可以在程序运行期间根据需要动态的创建代理类及其实例,来完成具体的功能
  • 2.动态代理
    动态代理类:在程序运行时,运用反射机制动态创建而成。

静态代理通常只代理一个类,动态代理是代理一个接口下的多个实现类。
静态代理事先知道要代理的是什么,而动态代理不知道要代理什么东西,只有在运行时才知道。

静态代理Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public interface Iuser {
  void eat(String s);
}
public class UserImpl implements Iuser {
  @Override
  public void eat(String s) {
    System.out.println("我要吃"+s);
  }
}
public class DynamicProxy implements InvocationHandler {
  private Object object;//用于接收具体实现类的实例对象
  //使用带参数的构造器来传递具体实现类的对象
  public DynamicProxy(Object obj){
    this.object = obj;
  }
  @Override
  public Object invoke(Object proxy, Method method, Object[] args)throws Throwable {
    System.out.println("前置内容");
    method.invoke(object, args);
    System.out.println("后置内容");
    return null;
  }
}
public class ProxyTest {
  public static void main(String[] args) {
    Iuser user = new UserImpl();
    InvocationHandler h = new DynamicProxy(user);
    Iuser proxy = (Iuser) Proxy.newProxyInstance(Iuser.class.getClassLoader(), new Class[]{Iuser.class}, h);
    proxy.eat("苹果");
  }
}

动态代理Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public interface Iuser {
  void eat(String s);
}
public class UserImpl implements Iuser {
  @Override
  public void eat(String s) {
    System.out.println("我要吃"+s);
  }
}
public class DynamicProxy implements InvocationHandler {
  private Object object;//用于接收具体实现类的实例对象
  //使用带参数的构造器来传递具体实现类的对象
  public DynamicProxy(Object obj){
    this.object = obj;
  }
  @Override
  public Object invoke(Object proxy, Method method, Object[] args)throws Throwable {
    System.out.println("前置内容");
    method.invoke(object, args);
    System.out.println("后置内容");
    return null;
  }
}
public class ProxyTest {
  public static void main(String[] args) {
    Iuser user = new UserImpl();
    InvocationHandler h = new DynamicProxy(user);
    Iuser proxy = (Iuser) Proxy.newProxyInstance(Iuser.class.getClassLoader(), new Class[]{Iuser.class}, h);
    proxy.eat("苹果");
  }
}

4. 如何将一个Java对象序列化到文件

让对象实现Serializable接口或者Parcelable接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public void saveObjToFile(Person p){
try {
//写对象流的对象
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream(fileName));
oos.writeObject(p); //将Person对象p写入到oos中
oos.close(); //关闭文件流
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/*
* 从文件中读出对象,并且返回Person对象
*/
public Person getObjFromFile(){
try {
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(fileName));
Person person=(Person)ois.readObject(); //读出对象
return person; //返回对象
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}

5. Java反射的理解

反射的含义

反射(Reflection)是Java 程序开发语言的特征之一,它允许运行中的 Java 程序获取自身的信息,并且可以操作类或对象的内部属性

简而言之,通过反射,我们可以在运行时获得程序或程序集中每一个类型的成员和成员的信息。
程序中一般的对象的类型都是在编译期就确定下来的,而Java反射机制可以动态地创建对象并调用其属性,这样的对象的类型在编译期是未知的。所以我们可以通过反射机制直接创建对象,即使这个对象的类型在编译期是未知的。
 反射的核心是JVM在运行时才动态加载类或调用方法/访问属性,它不需要事先(写代码的时候或编译期)知道运行对象是谁。

Java反射框架主要提供以下功能:

  • 1.在运行时判断任意一个对象所属的类;
  • 2.在运行时构造任意一个类的对象;
  • 3.在运行时判断任意一个类所具有的成员变量和方法(通过反射甚至可以调用private方法);
  • 4.在运行时调用任意一个对象的方法

    重点:是运行时而不是编译时

反射的运用

1、获得Class对象


方法有三种

  • (1)使用Class类的forName静态方法:

    1
    public static Class<?> forName(String className)
    • (2)直接获取某一个对象的class,比如:
      1
      2
      Class<?> klass = int.class;
      Class<?> classInt = Integer.TYPE;
  • (3)调用某个对象的getClass()方法,比如:

    1
    2
    StringBuilder str = new StringBuilder("123");
    Class<?> klass = str.getClass();

2、判断是否为某个类的实例


一般地,我们用instanceof关键字来判断是否为某个类的实例。同时我们也可以借助反射中Class对象的isInstance()方法来判断是否为某个类的实例,它是一个Native方法:

1
2
public native boolean isInstance(Object obj);

3、创建实例


通过反射来生成对象主要有两种方式。

  • (1)使用Class对象的newInstance()方法来创建Class对象对应类的实例。

    1
    2
    Class<?> c = String.class;
    Object str = c.newInstance();
  • (2)先通过Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建实例。这种方法可以用指定的构造器构造类的实例。

    1
    2
    3
    4
    5
    6
    7
    //获取String所对应的Class对象
    Class<?> c = String.class;
    //获取String类带一个String参数的构造器
    Constructor constructor = c.getConstructor(String.class);
    //根据构造器创建实例
    Object obj = constructor.newInstance("23333");
    System.out.println(obj);

4、获取方法


获取某个Class对象的方法集合,主要有以下几个方法:
getDeclaredMethods()方法返回类或接口声明的所有方法,包括公共、保护、默认(包)访问和私有方法,但不包括继承的方法。

1
2
public Method[] getDeclaredMethods() throws SecurityException

getMethods()方法返回某个类的所有公用(public)方法,包括其继承类的公用方法。

1
public Method[] getMethods() throws SecurityException

getMethod方法返回一个特定的方法,其中第一个参数为方法名称,后面的参数为方法的参数对应Class的对象

1
public Method getMethod(String name, Class<?>... parameterTypes)

getMethod 中方法 需要是public,才能获取到

用例子说明一下,加深理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private void testReflect(){
try {
Class<?> cls = Class.forName("hard.uistudy.dai.uifinaltest.main.view.activity.MainActivity6");
try {
Object object = cls.newInstance();
Method[] methods = cls.getMethods();
Method[] declaredMethods = cls.getDeclaredMethods();
for (Method m: methods) {
Log.e("method",m.toString());
}
for (Method m: declaredMethods) {
Log.e("declaredMethods",m.toString());
}
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

5、获取构造器信息


获取类构造器的用法与上述获取方法的用法类似。主要是通过Class类的getConstructor方法得到Constructor类的一个实例,而Constructor类有一个newInstance方法可以创建一个对象实例:

1
2
public T newInstance(Object ... initargs)

此方法可以根据传入的参数来调用对应的Constructor创建对象实例~

6、获取类的成员变量(字段)信息


主要是这几个方法,在此不再赘述:
getFiled: 访问公有的成员变量
getDeclaredField:所有已声明的成员变量。但不能得到其父类的成员变量
getFileds和getDeclaredFields用法同上(参照Method)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void testReflect(){
try {
Class<?> cls = Class.forName("hard.uistudy.dai.uifinaltest.main.view.activity.MainActivity6");
try {
Object object = cls.newInstance();
Method[] methods = cls.getMethods();
Field[] fields = cls.getDeclaredFields();
for (Field field: fields ) {
Log.e("Field",field.toString());
}
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

7、调用方法


当我们从类中获取了一个方法后,我们就可以用invoke()方法来调用这个方法。invoke方法的原型为:

1
2
3
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException

参考下面的实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class test1 {
public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
Class<?> klass = methodClass.class;
//创建methodClass的实例
Object obj = klass.newInstance();
//获取methodClass类的add方法
Method method = klass.getMethod("add",int.class,int.class);
//调用method对应的方法 => add(1,4)
Object result = method.invoke(obj,1,4);
System.out.println(result);
}
}
class methodClass {
public final int fuck = 3;
public int add(int a,int b) {
return a+b;
}
public int sub(int a,int b) {
return a+b;
}
}

8、利用反射创建数组


数组在Java里是比较特殊的一种类型,它可以赋值给一个Object Reference。下面我们看一看利用反射创建数组的例子:

1
2
3
4
5
6
7
8
9
10
11
12
public static void testArray() throws ClassNotFoundException {
Class<?> cls = Class.forName("java.lang.String");
Object array = Array.newInstance(cls,25);
//往数组里添加内容
Array.set(array,0,"hello");
Array.set(array,1,"Java");
Array.set(array,2,"fuck");
Array.set(array,3,"Scala");
Array.set(array,4,"Clojure");
//获取某一项的内容
System.out.println(Array.get(array,3));
}

注意事项

由于反射会额外消耗一定的系统资源,因此如果不需要动态地创建一个对象,那么就不需要用反射。
另外,反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。


原文: http://www.sczyh30.com/posts/Java/java-reflection-1/  作者: sczyh30

Java 进阶 ------ 时间复杂度对比

前言

通常,对于一个给定的算法,我们要做 两项分析。第一是从数学上证明算法的正确性,这一步主要用到形式化证明的方法及相关推理模式,如循环不变式、数学归纳法等。而在证明算法是正确的基础上,第二部就是分析算法的时间复杂度。算法的时间复杂度反映了程序执行时间随输入规模增长而增长的量级,在很大程度上能很好反映出算法的优劣与否。因此,作为程序员,掌握基本的算法时间复杂度分析方法是很有必要的。
算法执行时间需通过依据该算法编制的程序在计算机上运行时所消耗的时间来度量。而度量一个程序的执行时间通常有两种方法。

1.算法的效率

虽然计算机能快速的完成运算处理,但实际上,它也需要根据输入数据的大小和算法效率来消耗一定的处理器资源。要想编写出能高效运行的程序,我们就需要考虑到算法的效率。
算法的效率主要由以下两个复杂度来评估:
时间复杂度:评估执行程序所需的时间。可以估算出程序对处理器的使用程度。
空间复杂度:评估执行程序所需的存储空间。可以估算出程序对计算机内存的使用程度。

设计算法时,一般是要先考虑系统环境,然后权衡时间复杂度和空间复杂度,选取一个平衡点。不过,时间复杂度要比空间复杂度更容易产生问题,因此算法研究的主要也是时间复杂度,不特别说明的情况下,复杂度就是指时间复杂度。

2.时间复杂度

时间频度
一个算法执行所耗费的时间,从理论上是不能算出来的,必须上机运行测试才能知道。但我们不可能也没有必要对每个算法都上机测试,只需知道哪个算法花费的时间多,哪个算法花费的时间少就可以了。并且一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)。

时间复杂度
前面提到的时间频度T(n)中,n称为问题的规模,当n不断变化时,时间频度T(n)也会不断变化。但有时我们想知道它变化时呈现什么规律,为此我们引入时间复杂度的概念。一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数,记作T(n)=O(f(n)),它称为算法的渐进时间复杂度,简称时间复杂度。

3.大O表示法

像前面用O( )来体现算法时间复杂度的记法,我们称之为大O表示法。
算法复杂度可以从最理想情况、平均情况和最坏情况三个角度来评估,由于平均情况大多和最坏情况持平,而且评估最坏情况也可以避免后顾之忧,因此一般情况下,我们设计算法时都要直接估算最坏情况的复杂度。
大O表示法O(f(n)中的f(n)的值可以为1、n、logn、n²等,因此我们可以将O(1)、O(n)、O(logn)、O(n²)分别可以称为常数阶、线性阶、对数阶和平方阶,那么如何推导出f(n)的值呢?我们接着来看推导大O阶的方法。

推导大O阶

推导大O阶,我们可以按照如下的规则来进行推导,得到的结果就是大O表示法:
1.用常数1来取代运行时间中所有加法常数。
2.修改后的运行次数函数中,只保留最高阶项
3.如果最高阶项存在且不是1,则去除与这个项相乘的常数。

常数阶

先举了例子,如下所示。

1
2
3
int sum = 0,n = 100; //执行一次
sum = (1+n)*n/2; //执行一次
System.out.println (sum); //执行一次

上面算法的运行的次数的函数为f(n)=3,根据推导大O阶的规则1,我们需要将常数3改为1,则这个算法的时间复杂度为O(1)。如果sum = (1+n)*n/2这条语句再执行10遍,因为这与问题大小n的值并没有关系,所以这个算法的时间复杂度仍旧是O(1),我们可以称之为常数阶。

线性阶

线性阶主要要分析循环结构的运行情况,如下所示。

1
2
3
4
for(int i=0;i<n;i++){
//时间复杂度为O(1)的算法
...
}

上面算法循环体中的代码执行了n次,因此时间复杂度为O(n)。

对数阶

接着看如下代码:

1
2
3
4
5
6
int number=1;
while(number<n){
number=number*2;
//时间复杂度为O(1)的算法
...
}

可以看出上面的代码,随着number每次乘以2后,都会越来越接近n,当number不小于n时就会退出循环。假设循环的次数为X,则由2^x=n得出x=log₂n,因此得出这个算法的时间复杂度为O(logn)。

平方阶

下面的代码是循环嵌套:

1
2
3
4
5
6
for(int i=0;i<n;i++){
for(int j=0;j<n;i++){
//复杂度为O(1)的算法
...
}
}

内层循环的时间复杂度在讲到线性阶时就已经得知是O(n),现在经过外层循环n次,那么这段算法的时间复杂度则为O(n²)。
接下来我们来算一下下面算法的时间复杂度:

1
2
3
4
5
6
for(int i=0;i<n;i++){
for(int j=i;j<n;i++){
//复杂度为O(1)的算法
...
}
}

需要注意的是内循环中int j=i,而不是int j=0。当i=0时,内循环执行了n次;i=1时内循环执行了n-1次,当i=n-1时执行了1次,我们可以推算出总的执行次数为:

n+(n-1)+(n-2)+(n-3)+……+1
=(n+1)+[(n-1)+2]+[(n-2)+3]+[(n-3)+4]+……
=(n+1)+(n+1)+(n+1)+(n+1)+……
=(n+1)n/2
=n(n+1)/2
=n²/2+n/2

根据此前讲过的推导大O阶的规则的第二条:只保留最高阶,因此保留n²/2。根据第三条去掉和这个项的常数,则去掉1/2,最终这段代码的时间复杂度为O(n²)。

其他常见复杂度

除了常数阶、线性阶、平方阶、对数阶,还有如下时间复杂度:

  • f(n)=nlogn时,时间复杂度为O(nlogn),可以称为nlogn阶。
  • f(n)=n³时,时间复杂度为O(n³),可以称为立方阶。
  • f(n)=2ⁿ时,时间复杂度为O(2ⁿ),可以称为指数阶。
  • f(n)=n!时,时间复杂度为O(n!),可以称为阶乘阶。
  • f(n)=(√n时,时间复杂度为O(√n),可以称为平方根阶。

4.复杂度的比较

下面将算法中常见的f(n)值根据几种典型的数量级来列成一张表,根据这种表,我们来看看各种算法复杂度的差异。

n logn √n nlogn 2ⁿ n!
5 2 2 10 25 32 120
10 3 3 30 100 1024 3628800
50 5 7 250 2500 约10^15 约3.0*10^64
100 6 10 600 10000 约10^30 约9.3*10^157
1000 9 31 9000 1000 000 约10^300 约4.0*10^2567

从上表可以看出,O(n)、O(logn)、O(√n )、O(nlogn )随着n的增加,复杂度提升不大,因此这些复杂度属于效率高的算法,反观O(2ⁿ)和O(n!)当n增加到50时,复杂度就突破十位数了,这种效率极差的复杂度最好不要出现在程序中,因此在动手编程时要评估所写算法的最坏情况的复杂度。

下面给出一个更加直观的图:
这里写图片描述

其中x轴代表n值,y轴代表T(n)值(时间复杂度)。T(n)值随着n的值的变化而变化,其中可以看出O(n!)和O(2ⁿ)随着n值的增大,它们的T(n)值上升幅度非常大,而O(logn)、O(n)、O(nlogn)随着n值的增大,T(n)值上升幅度则很小。
常用的时间复杂度按照耗费的时间从小到大依次是:

O(1)<O(logn)<O(n)<O(nlogn)<O(n²)<O(n³)<O(2ⁿ)<O(n!)

参考

https://blog.csdn.net/itachi85/article/details/54882603

Android 进阶 ------ JVM DVM ART对比

Android系统使用Dalvik Virtual Machine (DVM)作为其虚拟机,所有安卓程序都运行在安卓系统进程里,每个进程对应着一个Dalvik虚拟机实例。他们都提供了对象生命周期管理、堆栈管理、线程管理、安全和异常管理以及垃圾回收等重要功能,各自拥有一套完整的指令系统。

Android之所以不直接使用JVM作为其虚拟机的原因有很多,版权问题我们暂且搁置一边,本文将首先在技术上对DVM和JVM进行比较,然后重点对Dalvik虚拟机的垃圾回收机制进行介绍,文章末尾再对Android5.0之后使用的新型虚拟机——ART虚拟机进行简单介绍

DVM vs JVM

共同点:
  • 都是解释执行
  • 都是每个 OS 进程运行一个 VM,并运行一个单独的程序
  • 在较新版本中(Froyo / Sun JDK 1.5)都实现了相当程度的 JIT compiler(即时编译) 用于提速。
    • JIT(Just In Time,即时编译技术)对于热代码(使用频率高的字节码)直接转换成汇编代码;
不同点:
  • dvm执行的是.dex格式文件,jvm执行的是.class文件。class文件和dex之间可以相互转换具体流程如下图,多个class文件转变成一个dex文件会引发一些问题,具体如下:

    • 方法数受限:多个class文件变成一个dex文件所带来的问题就是方法数超过65535时报错,由此引出MultiDex技术,具体资料同学可以google下。
    • class文件去冗余:class文件存在很多的冗余信息,dex工具会去除冗余信息(多个class中的字符串常量合并为一个,比如对于Ljava/lang/Oject字符常量,每个class文件基本都有该字符常量,存在很大的冗余),并把所有的.class文件整合到.dex文件中。减少了I/O操作,提高了类的查找速度。
  • 许多GC实现都是在对象开头的地方留一小块空间给GC标记用。Dalvik VM则不同,在进行GC的时候会单独申请一块空间,以位图的形式来保存整个堆上的对象的标记,在GC结束后就释放该空间。 (关于这一点后面的Dalvik垃圾回收机制还会更加深入的介绍)

  • dvm是基于寄存器的虚拟机 而jvm执行是基于虚拟栈的虚拟机。这类的不同是最要命的,因为它将导致一系列的问题,具体如下:
    • dvm速度快!寄存器存取速度比栈快的多,dvm可以根据硬件实现最大的优化,比较适合移动设备。JAVA虚拟机基于栈结构,程序在运行时虚拟机需要频繁的从栈上读取写入数据,这个过程需要更多的指令分派与内存访问次数,会耗费很多CPU时间。
      • 指令数小!dvm基于寄存器,所以它的指令是二地址和三地址混合,指令中指明了操作数的地址;jvm基于栈,它的指令是零地址,指令的操作数对象默认是操作数栈中的几个位置。这样带来的结果就是dvm的指令数相对于jvm的指令数会小很多,jvm需要多条指令而dvm可能只需要一条指令。
      • jvm基于栈带来的好处是可以做的足够简单,真正的跨平台,保证在低硬件条件下能够正常运行。而dvm操作平台一般指明是ARM系统,所以采取的策略有所不同。需要注意的是dvm基于寄存器,但是这也是个映射关系,如果硬件没有足够的寄存器,dvm将多出来的寄存器映射到内存中。

Dalvik虚拟机

谈到垃圾回收自然而然的想到了堆,Dalvik的堆结构相对于JVM的堆结构有所区别,这主要体现在Dalvik将堆分成了Active堆和Zygote堆,这里大家只要知道Zygote堆是Zygote进程在启动的时候预加载的类、资源和对象(具体gygote进程预加载了哪些类,详见文末的附录),除此之外的所有对象都是存储在Active堆中的。对于为何要将堆分成gygote和Active堆,这主要是因为Android通过fork方法创建到一个新的gygote进程,为了尽可能的避免父进程和子进程之间的数据拷贝,fork方法使用写时拷贝技术,写时拷贝技术简单讲就是fork的时候不立即拷贝父进程的数据到子进程中,而是在子进程或者父进程对内存进行写操作时是才对内存内容进行复制,Dalvik的gygote堆存放的预加载的类都是Android核心类和java运行时库,这部分内容很少被修改,大多数情况父进程和子进程共享这块内存区域。通常垃圾回收重点对Active堆进行回收操作,Dalvik为了对堆进行更好的管理创建了一个Card Table、两个Heap Bitmap和一个Mark Stack数据结构。

  • 关于 Zygote,有一些参考资料
    ZYGOTE浅谈

    Dalvik创建对象流程

    当Dalvik虚拟机的解释器遇到一个new指令时,它就会调用函数Object dvmAllocObject(ClassObject clazz, int flags)。期间完成的动作有( 注意:Java堆分配内存前后,要对Java堆进行加锁和解锁,避免多个线程同时对Java堆进行操作。下面所说的堆指的是Active堆):

    1.调用函数dvmHeapSourceAlloc在Java堆上分配指定大小的内存,成功则返回,否则下一步。
    2.执行一次GC, GC执行完毕后,再次调用函数dvmHeapSourceAlloc在Java堆上分配指定大小的内存,成功则返回,否则下一步。
    3.首先将堆的当前大小设置为Dalvik虚拟机启动时指定的Java堆最大值,然后进行内存分配,成功返回失败下一步。这里调用的函数是dvmHeapSourceAllocAndGrow
    4.调用函数gcForMalloc来执行GC,这里的GC和第二步的GC,区别在于这里回收软引用对象引用的对象,如果还是失败抛出OOM异常。这里调用的函数是dvmHeapSourceAllocAndGrow

Dalvik回收对象流程

Dalvik的垃圾回收策略默认是标记擦除回收算法,即Mark和Sweep两个阶段。标记与清理的回收算法一个明显的区别就是会产生大量的垃圾碎片,因此程序中应该避免有大量不连续小碎片的时候分配大对象,同时为了解决碎片问题,Dalvik虚拟机通过使用dlmalloc技术解决,关于后者读者另行google。下面我们对Mark阶段进行简单介绍。

Mark阶段使用了两个Bitmap来描述堆的对象,一个称为Live Bitmap,另一个称为Mark Bitmap。Live Bitmap用来标记上一次GC时被引用的对象,也就是没有被回收的对象,而Mark Bitmap用来标记当前GC有被引用的对象。当Live Bitmap被标记为1,但是在Mark Bitmap中标记为0的对象表明该对象需要被回收。

此外在Mark阶段往往要求其它线程处于停止状态,因此Mark又分为并行和串行两种方式,并行的Mark分为两个阶段:1)、只标记gc_root对象,即在GC开始的瞬间被全局变量、栈变量、寄存器等所引用的对象,该阶段不允许垃圾回收线程之外的线程处于运行状态。2)、有条件的并行运行其它线程,使用Card
Table记录在垃圾收集过程中对象的引用情况。整个Mark 阶段都是通过Mark Stack来实现递归检查被引用的对象,即在当前GC中存活的对象。标记过程类似用一个栈把第一阶段得到的gc_root放入栈底,然后依次遍历它们所引用的对象(通过出栈入栈),即用栈数据结构实现了对每个gc_root的递归。

Android 进阶 ------ Handler系列之创建子线程Handler

上一篇我介绍了Handler机制的工作原理,默认情况下,ActivityThread类为我们创建的了主线程的Looper和消息队列,所以当你创建Handler之后发送消息的时候,消息的轮训和handle都是在ui线程进行的。这种情况属于子线程给主线程发消息,通知主线程更新ui…等,那么反过来,怎么才能让主线程给子线程发消息,通知子线程做一些耗时逻辑??

  之前的学习我们知道,Android的消息机制遵循三个步骤:

  • 1 创建当前线程的Looper  

  • 2 创建当前线程的Handler 

  • 3 调用当前线程Looper对象的loop方法

  看过之前文章的朋友会注意到,本篇我特意强调了“当前线程”。是的之前我们学习的很多都是Android未我们做好了的,譬如:创建主线程的Looper、主线程的消息队列…就连我们使用的handler也是主线程的。那么如果我想创建非主线程的Handler并且发送消息、处理消息,这一系列的操作我们应该怎么办那???不怎么办、凉拌~~~什么意思???依葫芦画瓢,依然遵循上面的三步走,直接上代码!!!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class ChildThreadHandlerActivity extends Activity {
private MyThread childThread;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler);
childThread = new MyThread();
childThread.start();
Handler childHandler = new Handler(childThread.childLooper){//这样之后,childHandler和childLooper就关联起来了。
public void handleMessage(Message msg) {
};
};
}
private class MyThread extends Thread{
public Looper childLooper;
@Override
public void run() {
Looper.prepare();//创建与当前线程相关的Looper
childLooper = Looper.myLooper();//获取当前线程的Looper对象
Looper.loop();//调用此方法,消息才会循环处理
}
}
}

代码如上,我们依然循序Android的三步走战略,完成了子线程Handler的创建,难道这样创建完了,就可以发消息了么?发的消息在什么线程处理?一系列的问题,怎么办?看代码!!!运行上述代码,我们发现一个问题,就是此代码一会崩溃、一会不崩溃,通过查看日志我们看到崩溃的原因是空指针。谁为空???查到是我们的Looper对象,怎么会那?我不是在子线程的run方法中初始化Looper对象了么?话是没错,但是你要知道,当你statr子线程的时候,虽然子线程的run方法得到执行,但是主线程中代码依然会向下执行,造成空指针的原因是当我们new Handler(childThread.childLooper)的时候,run方法中的Looper对象还没初始化。当然这种情况是随机的,所以造成偶现的崩溃。

  那怎么办?难道我们不能创建子线程Handler ???No!!!No!!!No!!!,你能想到的Android早就为我们实现好了,HandlerThread类就是解决这个问题的关键所在,看代码!!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class HandlerThreadActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler);
TextView textView = (TextView) findViewById(R.id.tv);
textView.setText("HandlerThreadActivity.class");
HandlerThread handlerThread = new HandlerThread("HandlerThread");
handlerThread.start();
Handler mHandler = new Handler(handlerThread.getLooper()){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
Log.d("HandlerThreadActivity.class","uiThread2------"+Thread.currentThread());//子线程
}
};
Log.d("HandlerThreadActivity.class","uiThread1------"+Thread.currentThread());//主线程
mHandler.sendEmptyMessage(1);
}
}

创建HandlerThread对象的时候,有个参数,是指定线程名字的。上面的代码不管运行多少次都不会奔溃!!!并且这种方法创建的handler的handleMessage方法运行在子线程中。所以我们可以在这里处理一些耗时的逻辑。到此我们完成了主线程给子线程发通知,在子线程做耗时逻辑的操作。

  下面我们去看看源码,看看为什么使用HandlerThread就可以避免空指针那?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Looper getLooper() {
if (!isAlive()) {
return null;
}
// If the thread has been started, wait until the looper has been created.
synchronized (this) {
while (isAlive() && mLooper == null) {
try {
wait();
} catch (InterruptedException e) {
}
}
}
return mLooper;
}

HandlerThread类的getLooper方法如上,我们看到当我们获取当前线程Looper对象的时候,会先判断当前线程是否存活,然后还要判断Looper对象是否为空,都满足之后才会返回给我Looper对象,否则处于等待状态!!既然有等待,那就有唤醒的时候,在那里那???我们发现HandlerThread的run方法中,有如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public void run() {
mTid = Process.myTid();
Looper.prepare();
synchronized (this) {
mLooper = Looper.myLooper();
notifyAll();
}
Process.setThreadPriority(mPriority);
onLooperPrepared();
Looper.loop();
mTid = -1;
}

说明了什么那???HandlerThread类start的时候,Looper对象就初始化了,并唤醒之前等待的。所以HandlerThread很好的避免了之前空指针的产生。所以以后要想创建非主线程的Handler时,我们用HandlerThread类提供的Looper对象即可。


参考
https://www.cnblogs.com/lang-yu/p/6228832.html

Android进阶 ——— 使用SparseArray和ArrayMap代替HashMap

在Android开发时,我们使用的大部分都是Java的api,比如HashMap这个api,使用率非常高,但是对于Android这种对内存非常敏感的移动平台,很多时候使用一些java的api并不能达到更好的性能,相反反而更消耗内存,所以针对Android这种移动平台,也推出了更符合自己的api,比如SparseArray、ArrayMap用来代替HashMap在有些情况下能带来更好的性能提升。

介绍它们之前先来介绍一下HashMap的内部存储结构,就明白为什么推荐使用SparseArray和ArrayMap

HashMap

HashMap内部是使用一个默认容量为16的数组来存储数据的,而数组中每一个元素却又是一个链表的头结点,所以,更准确的来说,HashMap内部存储结构是使用哈希表的拉链结构(数组+链表),如图:
这种存储数据的方法叫做拉链法

且每一个结点都是Entry类型,那么Entry是什么呢?我们来看看HashMap中Entry的属性:

final K key;
V value;
final int hash;
HashMapEntry next;

从中我们得知Entry存储的内容有key、value、hash值、和next下一个Entry,那么,这些Entry数据是按什么规则进行存储的呢?就是通过计算元素key的hash值,然后对HashMap中数组长度取余得到该元素存储的位置,计算公式为hash(key)%len,比如:假设hash(14)=14,hash(30)=30,hash(46)=46,我们分别对len取余,得到
hash(14)%16=14,hash(30)%16=14,hash(46)%16=14,所以key为14、30、46的这三个元素存储在数组下标为14的位置,如:

从中可以看出,如果有多个元素key的hash值相同的话,后一个元素并不会覆盖上一个元素,而是采取链表的方式,把之后加进来的元素加入链表末尾,从而解决了hash冲突的问题,由此我们知道HashMap中处理hash冲突的方法是链地址法,在此补充一个知识点,处理hash冲突的方法有以下几种:

  • 开放地址法
  • 再哈希法
  • 链地址法
  • 建立公共溢出区

讲到这里,重点来了,我们知道HashMap中默认的存储大小就是一个容量为16的数组,所以当我们创建出一个HashMap对象时,即使里面没有任何元素,也要分别一块内存空间给它,而且,我们再不断的向HashMap里put数据时,当达到一定的容量限制时(这个容量满足这样的一个关系时候将会扩容:HashMap中的数据量>容量*加载因子,而HashMap中默认的加载因子是0.75),HashMap的空间将会扩大,而且扩大后新的空间一定是原来的2倍,我们可以看put()方法中有这样的一行代码:

1
int newCapacity = oldCapacity * 2;

所以,重点就是这个,只要一满足扩容条件,HashMap的空间将会以2倍的规律进行增大。假如我们有几十万、几百万条数据,那么HashMap要存储完这些数据将要不断的扩容,而且在此过程中也需要不断的做hash运算,这将对我们的内存空间造成很大消耗和浪费,而且HashMap获取数据是通过遍历Entry[]数组来得到对应的元素,在数据量很大时候会比较慢,所以在Android中,HashMap是比较费内存的,我们在一些情况下可以使用SparseArray和ArrayMap来代替HashMap。

SparseArray

SparseArray比HashMap更省内存,在某些条件下性能更好,主要是因为它避免了对key的自动装箱(int转为Integer类型),它内部则是通过两个数组来进行数据存储的,一个存储key,另外一个存储value,为了优化性能,它内部对数据还采取了压缩的方式来表示稀疏数组的数据,从而节约内存空间,我们从源码中可以看到key和value分别是用数组表示:

1
2
private int[] mKeys;
private Object[] mValues;

我们可以看到,SparseArray只能存储key为int类型的数据,同时,SparseArray在存储和读取数据时候,使用的是二分查找法,我们可以看看:

1
2
3
4
5
6
7
8
public void put(int key, E value) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
...
}
public E get(int key, E valueIfKeyNotFound) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
...
}

也就是在put添加数据的时候,会使用二分查找法和之前的key比较当前我们添加的元素的key的大小,然后按照从小到大的顺序排列好,所以,SparseArray存储的元素都是按元素的key值从小到大排列好的。

而在获取数据的时候,也是使用二分查找法判断元素的位置,所以,在获取数据的时候非常快,比HashMap快的多,因为HashMap获取数据是通过遍历Entry[]数组来得到对应的元素。

添加数据

1
public void put(int key, E value)

删除数据

1
public void remove(int key)

or

1
public void delete(int key)

其实remove内部还是通过调用delete来删除数据的

获取数据

1
public E get(int key)

or

1
public E get(int key, E valueIfKeyNotFound)

该方法可设置如果key不存在的情况下默认返回的value

特有方法

在此之外,SparseArray还提供了两个特有方法,更方便数据的查询:
获取对应的key:

1
public int keyAt(int index)

获取对应的value:

1
public E valueAt(int index)

SparseArray应用场景:
虽说SparseArray性能比较好,但是由于其添加、查找、删除数据都需要先进行一次二分查找,所以在数据量大的情况下性能并不明显,将降低至少50%。

满足下面两个条件我们可以使用SparseArray代替HashMap:

数据量不大,最好在千级以内
key必须为int类型,这中情况下的HashMap可以用SparseArray代替:

1
2
3
HashMap<Integer, Object> map = new HashMap<>();
用SparseArray代替:
SparseArray<Object> array = new SparseArray<>();

ArrayMap

这个api的资料在网上可以说几乎没有,然并卵,只能看文档了
ArrayMap是一个映射的数据结构,它设计上更多的是考虑内存的优化,内部是使用两个数组进行数据存储,一个数组记录key的hash值,另外一个数组记录Value值,它和SparseArray一样,也会对key使用二分法进行从小到大排序,在添加、删除、查找数据的时候都是先使用二分查找法得到相应的index,然后通过index来进行添加、查找、删除等操作,所以,应用场景和SparseArray的一样,如果在数据量比较大的情况下,那么它的性能将退化至少50%。

添加数据

1
public V put(K key, V value)

获取数据

1
public V get(Object key)

删除数据

1
public V remove(Object key)

特有方法

它和SparseArray一样同样也有两个更方便的获取数据方法:

1
2
public K keyAt(int index)
public V valueAt(int index)

ArrayMap应用场景
数据量不大,最好在千级以内
数据结构类型为Map类型

1
ArrayMap<Key, Value> arrayMap = new ArrayMap<>();

【注】:如果我们要兼容aip19以下版本的话,那么导入的包需要为v4包

1
import android.support.v4.util.ArrayMap;

总结
SparseArray和ArrayMap都差不多,使用哪个呢?
假设数据量都在千级以内的情况下:

1、如果key的类型已经确定为int类型,那么使用SparseArray,因为它避免了自动装箱的过程,如果key为long类型,它还提供了一个LongSparseArray来确保key为long类型时的使用
2、如果key类型为其它的类型,则使用ArrayMap

参考


https://blog.csdn.net/u010687392/article/details/47809295

|