Friday, April 24, 2020

Just another regex...


Preface

I like regexes, I really do. And I also like perl, and feel like they both are heavily neglected nowadays and neither of them deserves it. OK, it indeed takes some effort to learn to use them correctly, but that's also true for a chainsaw. Or for a scalpel. Or for a tool that can be used as a chainsaw and as a scalpel and as a lightsaber and ...

OK, back to the regexes. It'll be python, its regex support is almost as good as in perl ;).

I ran into a task that requires some URL mangling: some hostnames have to be replaced by some new hostnames, and so on.

Such an URL looks like: scheme://user:pass@host:port/path1/path2/path3?arg1=val1&arg2=val2

The scheme is usually "http" or "https", but it can be "svn+ssh" as well, and all parts but the host are optional.

Yes, I know, officially the scheme is mandatory as well, but when you ask a user to enter the URL of a server, in 80% of the cases it won't start with "https://", but with a plain hostname. And real-life data comes from real-life users, and they don't care about the RFCs and if you try to enforce it, you'll be asked to remove that "pesky" check and imply that "http://" if there is nothing there.

First I wanted to avoid reinventing the wheel and tried to use the urllib.parse module, but it doesn't handle that "user:pass" part well (for parsing it's said to handle but doesn't, for "unparsing" it's not even considered). Besides, it returns named tuples of ParseResult type, and as they are tuples, they're immutable, so any modification would require creating new instances of them. And there is no such thing as "copy-all-attributes-but-the-hostname" constructor, so I would've had to list each and every attribute for the normal constructor. Which doesn't handle separate username and password, so it would've had to be pre-mangled into the hostname... barf... sorry...

Urllib is not aimed for this task. I'm not yet sure what exactly it's aimed for, but it wouldn't make this task much simpler.

I don't need that immutability, I want a plain class that can parse the input string, lets me access (r+w) the fields and can rebuild them to an output string.

And if I have to write wrapper code to enclose some gory junk, then I prefer it to be my own junk.

It wasn't a hard task, but it lead to this steroid-filled monster regex golem:

^(?:([^:]*)://)?(?:([^:@]*)(?::([^@]*))?@)?([^:/?]*)(?::([0-9]*))?(/[^?]*)?(?:[?](.*))?

Actually, it's much simpler that how it looks, we just have to build it step by step, but it indeed needs the explanation. Which I don't want to embed into the code comments, just the URL of this post.

Even the longest journey begins with a single small step, so:

The structure of the URLs

Let's make it clear what we want to parse:

  • At the beginning of the string
  • There is an optional scheme: anything but ":" characters, followed by "://"
  • Then the also optional auth part:
    • A user part: anything but ":" or "@" characters
    • Then an optional password part: a ":", followed by anything but "@" characters
    • And then an "@" character
  • Then the host part: anything but ":" or "/" or "?" characters
  • Then the optional port part: a ":", followed by digits
  • Then the optional path part: a "/", followed by anything but "?" characters
  • Then the optional args part: a "?", followed by the rest
Let's begin with the scheme part. It'll be a bit longer, as the concepts are explained here as well.

The scheme part

At the beginning of the string, a sequence of anything but ":" characters, followed by "://"
That is: ^[^:]*://
  • ^ matches the beginning of the string
  • [^:] matches one character that is anything but ":"
  • so [^:]* matches (the longest possible) sequence of zero or more such characters
  • : is not a special character, so it matches a ":"
  • / is not a special character either, so it matches a "/"
  • so :// matches the literal string "://"
So ^[^:]*:// matches the scheme prefix followed by "://".

"Matching" only means a yes/no decision whether the text is in this format or not, but doesn't return us anything. However, we want to extract (the term is "capture") that part that was matched by that [^:]*, and this is what grouping does: if we enclose this part of the pattern in parentheses, then the regex engine will (at the end) return us the part of the input matched by this part of the pattern:

^([^:]*)://

Note that we're interested only in the scheme, like "https", and not the "://" that follows it, so that's why I left the latter one outside of the grouping. That is only matched for, but not to be captured.

If we match any input against this regex like x = test_regex.search("https://some.host.com"), then x.group(1) will contain "https". We're almost there :).

There is one more thing with the scheme part: it's optional. If there is no "://" in the input, then it's still valid, it's just the host part follows a "missing" scheme part.

In regex, the multiplicity sign for "0 or 1 occurences" is ?, and it is applied to whatever atomic unit it follows: either a single character, or a grouped sub-expression. The regex aab?cc would match "aabcc" or "aacc": "aa", followed by 1 or 0 occurences of "b", followed by "cc".

We want to use it to check the occurence count of not one character, but a sub-pattern group, so it will follow not a character, but a parenthesized grouping: ( ... )?

Namely, the whole scheme part (now with extra spaces for readability): ^  (  ([^:]*)://  )?

The start of the line, then our scheme part enclosed in a new group, which must have the cardinality of 0 or 1.

And this would work!

But now that we have defined two groups in the pattern, we would get two groups in the result as well:
  • x.group(1) would return the text matched by the 1st starting group: "https://" and
  • x.group(2) would return the text matched by the 2nd starting group: "https"
This would ruin the result group numbering and in fact we don't want the text matched by the 1st (the outer) parenthesized group, we just needed it to mark the scope of that ?.

In extended regex syntax there is a non-capturing group syntax: it's like a normal group, only it doesn't bother to extract the matched text: (?: ... )

Yes, nasty syntax. It wasn't part of the original "basic" regex, and he other braces already had their special meaning, so for such extended features an otherwise meaningless, invalid syntax was introduced, the (?...) notation. Among which the (?: ... ) means the non-capturing groups.

Replacing the 1st (outer) parentheses of our scheme-capturing regex:
Instead of ^  (  ([^:]*)://  )? it becomes ^  (?:  ([^:]*)://  )?

And the without those extra spaces, the real one: ^(?:([^:]*)://)?

The authenticity part

  • A user part: anything but ":" or "@" characters
  • Then an optional password part: a ":", followed by anything but "@" characters
  • And then an "@" character
User part: ([^:@]*)
We've already seen patterns like this, in the scheme part regex.

Password part: :  ([^@]*)
Remember: the leading ":" is just matched for, but we don't need to capture it.

The optional password part: (?:  :([^@]*)  )?
Remember: non-capturing group, 0 or 1 times.

The trailing "@": @
It's not a special character, so it matches itself :).

So the authenticity part together (with didactic spaces): ([^:@]*)  (?::([^@]*))?  @

These non-capturing-groups make it seem more mystical than how it actually is, but that's the
only way to deal with optional parts.

Apropos optional, this whole authenticity is also optional, so it needs a surrounding
(?: ... )? construct: (?:  ([^:@]*)(?::([^@]*))?@  )?

And the without those extra spaces, the real one: (?:([^:@]*)(?::([^@]*))?@)?

(Hint: Don't try to interpret the whole pattern char-by-char. Just follow the way it evolved from simpler sub-patterns, and what those sub-patterns do, and not how they do it. If you want to analyze those sub-patterns, then do it separately, without the complexity of the bigger composite pattern.)


The host part

It's not optional, so it'll be way simpler than the previous ones. A sequence of any characters but ":", "/" or "?", all of it captured as a group.

This is it: ([^:/?]*)


The port part

Starts with a ":", followed by a sequence of digits, which should be captured as a group, so it would be: :([0-9]*)

But it's optional, so: (?:  :([0-9]*)  )?

Without the extra spaces: (?::([0-9]*))?


The path part

Starts with a "/", followed by a sequence of non-"?" characters, all of it captured as a group, which is optional (i.e. occurs 0 or 1 times)

This is it: (/[^?]*)?

Note that since we already have the whole 0-or-1-times part as a group, there is no need for the usual non-capturing `(?: ... )` grouping, but we can affix the "?" right after our plain capturing group.


The arguments part

Starts with a "?", followed by anything (that is, a sequence of any characters).

By now you surely recall that in regex "?" has a special meaning, so how can we match the literal "?" character?

Either as \?, which is good, but as our regex will be a string in python, and python would interpret this as a python backslash-escape sequence (and thus produce a single ? into the regex, making it possibly invalid), so we would need to write it as \\?, from which python would make the \? we actually want.

IMHO regex can be ugly enough by its own means, no need to add some string escaping to it :D.

So I prefer the other syntax, which uses the fact that within a character set, all characters lose their special meaning (well, except for ^ and - and ], except at certain places, but that's another story), so the regex [?] matches any character that is "?" :).

So a "?" followed by any sequence of any characters: [?]  .*

Of which we want to capture that sequence: [?]  (.*)

And this part is optional, so: (?:  [?](.*)  )?

And the without those extra spaces, the real one: (?:[?](.*))?


All parts together

The moment of truth :D

- The scheme: ^(?:([^:]*)://)?
- The authenticity: (?:([^:@]*)(?::([^@]*))?@)?
- The host: ([^:/?]*)
- The port: (?::([0-9]*))?
- The path: (/[^?]*)?
- The args: (?:[?](.*))?

All together, in this order: 
^(?:([^:]*)://)?  (?:([^:@]*)(?::([^@]*))?@)?  ([^:/?]*)  (?::([0-9]*))?  (/[^?]*)?  (?:[?](.*))?

Without the extra spaces:
^(?:([^:]*)://)?(?:([^:@]*)(?::([^@]*))?@)?([^:/?]*)(?::([0-9]*))?(/[^?]*)?(?:[?](.*))?

Don't try to pronounce it, on bad days it may summon the Great Cthulhu and he'll reopen all your bugs. But when matching an URL-like string against it, we get the following results:

$ python3
...
>>> import re

>>> url_split = re.compile("^(?:([^:]*)://)?(?:([^:@]*)(?::([^@]*))?@)?([^:/?]*)(?::([0-9]*))?(/[^?]*)?(?:[?](.*))?")

>>> x = url_split.search("https://user1:password1@host1:8080/p1/p2/p3?qwer=42&asdf=111")

>>> x.group(1)
'https'
>>> x.group(2)
'user1'
>>> x.group(3)
'password1'
>>> x.group(4)
'host1'
>>> x.group(5)
'8080'
>>> x.group(6)
'/p1/p2/p3'
>>> x.group(7)
'qwer=42&asdf=111'

Try it with some of the optional parts missing, their respective group()-s shall return None.

Feelin' the power of regular expressions? We've barely scratched the surface! Here is the book about the subject.

Conclusion

That was it.

A big write-only lump of ascii junk, whose readability wouldn't be any worse after a cryptographically strong encryption either.

On the other hand, a fast and robust tool, that, when seen along its evolution, isn't random at all, but it's a concise and logical description of what we wanted to recognize.

It's just... another regex :D.

I hope you enjoyed this journey!
(For sure I did. It felt like I'm solving problems again, and not just glueing together some pre-built modules.)

And I also hope that the next time you face such a problem, you won't start mucking around with index() and substr() and state flags and multi-level if-elif-elif-else constructs...

Wednesday, June 13, 2018

Git server and Jira on Debian

Nothing special, just yet another "I've done it this way" installation log.
But hey, did you know that the word "blog" comes from "web log"? So here it is :D.

Base OS

Start from a minimal Debian installation.

Git server

Start from this howto.

It's not 100%, because some config formats have changed since then, so here are the differences:

Repo name

Where it says repo1.git, choose your own repo name. Choose a descriptive one and save the constant need for explanation.

Git config files

It uses a separate /etc/gitdata, I found /opt/allgit more convenient:

* It's also not publicly accessible
* All related files are at the same place

So I put gitusers.passwd and the certificates also here.

The SSL certificate

When creating the cert as the howto tells to, openssl will ask about the identity for whom to create the cert. For the CN (Common Name) enter the public hostname of your server, the rest is up to you, technically it doesn't matter.

Besides, the command in the blog indeed generates a self-signed cert, but also marks it as a CA cert (because that it is).

This is fine, but Apache will dump warnings in the log:
AH01906: <your public hostname>:443:0 server certificate is a CA certificate (BasicConstraints: CA == TRUE !?)

Normally you would have one cert and its private key for your server, and one cert (without its priv key) from the CA authority that testifies for your server cert.

Here you have one certificate and the private key that belongs to it, that's all, and this certificate plays
both roles: it acts as a server cert, and also a CA cert that has signed your server cert ('self-signed', it is).

Apache modules

The actual command is a2enmod ssl cgi alias env.

Apache site config

cp default-ssl mygitdomain.com.conf

Don't forget to append .conf!

Then, you'll need a bunch of small fixes:

* An enclosing VirtualHost definition
* Logfile name: we'll also run Jira, Confluence, maybe more, so a domain-related log filename is more descriptive than the git-related one used by the howto

So here is the site config:
<VirtualHost _default_:443>

        ServerName <your public hostname>
        ServerAdmin webmaster@localhost
        ErrorLog /var/log/apache2/<your public hostname>.log

        SSLEngine on
        SSLCertificateFile    /opt/allgit/mygit_ssl.crt
        SSLCertificateKeyFile /opt/allgit/mygit_ssl.key

        # ==== Generic public space

        DocumentRoot /opt/allgit/public

        <Directory "/opt/allgit/public">
                Options Indexes FollowSymLinks
                AllowOverride None
                Require all granted
        </Directory>

        # ==== for the git repo

        SetEnv GIT_PROJECT_ROOT /opt/allgit
        SetEnv GIT_HTTP_EXPORT_ALL
        SetEnv REMOTE_USER=$REDIRECT_REMOTE_USER
        ScriptAlias /allgit/ /usr/lib/git-core/git-http-backend/

        <Directory "/usr/lib/git-core/">
                AllowOverride None
                Options +ExecCGI -Includes
                Require all granted
        </Directory>

        <LocationMatch "^/allgit/repos/.*$">
                AuthType Basic
                AuthName "My Git Repositories"
                AuthUserFile /opt/allgit/gitusers.passwd
                Require valid-user
        </LocationMatch>

</VirtualHost>

# vim: syntax=apache ts=4 sw=4 sts=4 sr noet

A public http space

Your git repos will be accessible as https://<your public hostname>/allgit/repos/some_repo.git, that's fine.

But you may need a generic https://<your public hostname>/<whatever> space, at least for not displaying the default Apache placeholder.

mkdir -p /opt/allgit/public

Whatever you put in this folder, it'll be accessible like the generic link above.

About the Certificate Verification Error

A self-signed cert tells the client that you are who you say you are, just because you say so.

If the client accepts you as trustworthy, then believes what you say, that you are who you say you are :). If he accepts you as trustworthy, that is, otherwise your browser will throw those 'security alert' windows at you, and the commandline clients (like git) will just exit with an appropriate error.

So, you must add your CA certificate to the 'trusted' group at your client.

Disabling the cert checks for git

The howto tells you can, and it indeed would work, but believe me, it's a bad idea.

Anyway, here's the command: git config --global http.sslVerify false

However, you'd rather add the cert to the client's trusted group, it's not that complex.

Adding your CA certificate to the client's trusted group

The client has a folder in which it contains all the CA certificates it considers trusted, you shall
put your CA cert (in this case, the cert), but the filename must be a special one instead of mygit_ssl.crt.

Why (skip this if not interested)

There is a technical reason for it: When the client receives a cert from a server, it wants to check whether it was signed by a trusted CA, so theoretically it should look up all the trusted CA certs and check whether it has signed the server cert or not. This is time-consuming, because there can be quite a many of trusted CA certs.

On the other hand, if the filename of the required CA cert could be identified by some means, then there would be no need to check all the CA certs, it would be enough to read just the required one.

This is achieved by a checksum-like trick: for all CA cert a checksum is calculated from the name of the authority they represent, and this checksum gives the beginning part of the filename.

(NOTE: I say checksum and name of the authority, but in fact there's a more complex hash algorithm used and it's applied to the full Distinguished Name of the issuer, not to the human-readable one. Anyway, the principle is the same.)

So when the client sees a server cert, it just reads the name of the authority that has (allegedly) signed that server cert, calculates that checksum for it, and if there indeed is a trusted CA cert representing that authority, its filename will begin with that checksum, so the client must check only a few trusted CA cert files instead of all of them.

Why few and not just the one? This is a checksum, necessarily shorter than the information from which it is calculated, so theoretically there can be more than one CA authorities whose name gives the same checksum.

Therefore the actual name of the file is like <checksum>.<idx> where this idx is just an incremental number, starting from 0, so if there are two CAs whose names give the same checksum 12345678, then their filenames will be 12345678.0 and 12345678.1 respectively.

How to get this filename

cp /opt/allgit/mygit_ssl.crt /opt/allgit/public/$(openssl x509 -noout -hash -in /opt/allgit/mygit_ssl.crt).0

ls -l /opt/allgit/public/

Now we've copied our certificate to the publicly available space (it may be published, just the private key is to be kept secret), with an appropriate name.

On the client you may download it with a browser like https://<your public hostname>/12345678.0

What to do on the client side

Create a folder if it doesn't yet exist: mkdir -p /etc/ssl/certs, download the cert file into it.

Make it world readable but writable only for the owner:
chmod 644 /etc/ssl/certs/12345678.0

make it owned by root:
chown root:root /etc/ssl/certs/12345678.0

And finally git to use it system-wide:
git config --system http.sslCAPath /etc/ssh/certs

This way you configured it for all users on the client machine.

If you are just one user, and don't have root access, then create the folder as ~/certs and
tell git to set only your global config (instead of the system-wide one):
git config --global http.sslCAPath ~/certs

Jira

A few words in advance.

Jira will set up itself as an independent web server that listens on http://localhost:8080/whatever, that is, separate port, no encryption, and everything on this 'host:port' is assumed to belong to Jira.

But our server is not dedicated just to it, we'd like to access it at https://<your public hostname>/jira/whatever, that is, standard https port, SSL encryption, and only the part after /jira belongs to Jira.

This is possible, but we need some configurations to change a bit.

The second issue is that Jira and the IPv6 support of Debian are not fully compatible, so we'll need to disable IPv6.

I've tried the mentioned _JAVA_OPTIONS setting, it didn't work.
The symptoms were: after installation Jira starts fine and listens on 8080, but if I stop and restart it
as a service (or manually, doesn't matter), it starts, but doesn't listen on 8080.

So, roll up your sleeve, take a sip of your coffee, and let's begin :).

Disable IPv6

Edit /etc/sysctl.conf as described here.

In addition, edit /etc/ssh/sshd_config and change AddressFamily any to AddressFamily inet (uncomment it as well, if it's still commented out).

Then reboot the server - sorry, no way to do it online.

Set up Apache to act as Reverse Proxy

Reverse proxying means that technically the Apache will receive the incoming https://.../jira/whatever requests, but instead of processing them by itself, it'll pass them on to Jira, with rewriting the URLs as needed.

There is a nice description of the concept here, but as usual, it's not 100% applicable, things have changed a bit since then.

Some more apache modules

a2enmod proxy proxy_http proxy_connect

Edit /etc/apache2/mods-enabled/proxy.conf and uncomment the Proxy block.

Site config

Edit /etc/apache2/sites-available/<your public hostname>.conf again:

<VirtualHost _default_:443>
    ...

    # ==== for Jira
    ProxyRequests On
    ProxyVia On
    <Location "/jira">
        Options Indexes FollowSymLinks
        AllowOverride None
        Require all granted
        ProxyPass "http://127.0.0.1:8080/jira"
        ProxyPassReverse "http://127.0.0.1:8080/jira"
    </Location>

</VirtualHost>

# vim: syntax=apache ts=4 sw=4 sts=4 sr noet

This tells Apache that whenever it gets something that starts with /jira, it shall pass it on
to Jira, by rewriting that /jira to http://127.0.0.1:8080/jira (keeping whatever follows in the URL).

NOTE: By default Jira will expect http://127.0.0.1:8080/, so we must reconfigure it to use the /jira path instead of just /.

Restart Apache

Easy to forget, annoying to debug :).

systemctl restart apache2

Install Jira

Just follow the official howto. For the impatient, here is the current installer for linux/x86_64.

The default installer method is fine (no need for custom), only at the last question, whether it shall start Jira right away, should you say 'no'.

Configure Jira to use the '/jira' path

Open this howto, and see 'Step 1 Configure Tomcat', and '2 set the context path' within it.

The other sub-steps are NOT needed, they refer to a different situation, when the Jira server is completely separated from the internet, which is not the case here.

Start Jira

The moment of truth :D.

systemctl start jira

top

Wait until it settles and the load goes down (it does some initialisation before actually starts listening), then check if it managed to start up:

ps axufww | grep java

If you see a multi-line crap of a command that starts with /opt/atlassian/jira/jre//bin/java, you're halfway home.

If not, then see the logs in /opt/atlassian/jira/logs/, but they are not the most straightforward kind, prepare for heavy googling and you may as well open stackoverflow.com too.

Check if it listens for incoming connections:

netstat -4nlp | grep 8080

If you see the process above (like 1118/java), your Jira is up, so if Apache passes some requests to it, it may even work.

If you seen nothing here, regardless of the fact that the Jira process is running, you're deep in Trouble County. Check if IPv6 is indeed disabled (ifconfig shall show no inet6 addresses).

If it is, then I'm out of ideas, and I bet the logs won't help much either. Google, SO, try some magicks, some may even do the trick, provided that the rest hasn't wrecked your system to crap by the time you find the good ones... If you ran into it and found something useful, please leave a comment with the details (OS version, Jira version, the magick that killed the beast, etc.) for the rest of us!

PostgreSQL

You'll need a postgres to store the data for Jira and Confluence, and you may start with the official howto. Almost 100%, as usual.

As of Postgres config, as long as you want to access the DB only from localhost and only by users
whose unix username matches the database username (for Jira this is the case), its the default config will suffice.

Things that differ from the howto:

Version

Now it's 9.6 instead of 8.4.

Enable and start

Don't forget to enable and start the DB after installation:

systemctl enable postgresql

systemctl start postgresql

DB User

As of 9.6, the createdb command won't ask you questions, but expect the options for the role as
commandline options.

The right to create DBs is given by -d, so your command is createuser -P -d jira .

If you messed up, just dropuser jira and try again.

Choose a nice random password, it'll be needed only once, when you tell it to the Jira setup.

(My favourite password generator is the pwgen packagepwgen --no-numerals --no-capitalize 12, it dumps a screenload of more-or-less intelligible but still random passwords, pick one and use it. Far better than 'Jira!123', maybe a bit weaker than correct horse battery staple, but still meets most of the stupid password criteria...)

Jira setup

The final steps can be done via your browser.

On your client, open https://<your public hostname>/jira, you shall get a nice Jira setup page.
If you don't, check the apache logs in /var/log/apache2/<your public hostname>.log on the server, there's most probably some typo in the apache proxy setup (spaces and slashes do make a difference there!)

On the Jira setup page choose to do it for yourself, choose an external DB, set it to Postgres,
host=127.0.0.1, port=5432, db name is the db name you created, username is 'jira', password is the
nice random password you've chosen, Test DB Connection.

On the next screen set an App Title, and set the Base URL to https://<your public hostname>/jira!

That's all, you're done :D !

Tuesday, February 6, 2018

"Mérnök"

Mottó: "Ha az építészek úgy építkeznének, ahogy a programozók programoznak, akkor az első k.... harkály romba dönthetné az emberi civilizációt!"

 

 "Mérnök" és "ipar" vs. IT

A hétköznapjaink tele vannak az IT gyarlóságaival: már megint lefagyott az X app, az Y böngésző eszi a memóriát és rendszeresen újra kell indítani, a Z oprendszerben újabb kritikus sebezhetőséget fedeztek fel, és így tovább.

Ugyanakkor szintén naponta találkozunk az IT diadalaival: újabb képfelismerő rendszert fejlesztettek ki az X cégnél, az Y egyetemen tovább jutottak a szövegértési rendszerekkel, a Z technológia instant képi kommunikációt tesz lehetővé fillérekért, satöbbi.

Ilyesmiket valahogy sokkal ritkábban hallani a klasszikus mérnöki területekről, pl. a gépészet, vegyészet, villamos- és építészmérnöki  tudományok tájáról. Vajon miért?  Van-e netán összefüggés a kettő között, esetleg valami közös ok, amelyikből ezen két sajátosság ered?

Vegyük kicsit szemügyre először a hagyományos mérnökök munkájának azon vonásait, amik az IT-ben másképp vannak.

Hosszú lesz, de egyrészt úgyis munkaidőben vagyunk :), másrészt pedig szükség van a pontos képre.

Aki nagyon türelmetlen, az menjen a végére, van ott egy egyflekkes összefoglaló is :).

A hagyományos mérnöki tevékenység

Szabványok használata

Egy gépészmérnök csak a legszükségesebb esetben tervez új komponenst, ahol csak lehet, igyekszik már létező, szabványos, 'katalógusból rendelhető' elemekből tervezni, építkezni. Ezt diktálja egyrészt a gazdaságosság, hiszen így a kész rendszer tulajdonosának a későbbi pótalkatrész beszerzésénél könnyű a dolga, nem kell egyedi gyártmányt újragyártatnia, de ugyanebbe az irányba hat a megbízhatóság is, nevezetesen egy komponens csak akkor kap forgalomba hozatali engedélyt, ha annak van érdemi specifikációja (a paraméterek várható érteivel, szórásával, üzemi és extrém minimumával és maximumával), és ez rendszeres szúrópróbákkal ellenőrizve is van. A tervező mérnök tehát ezekre építhet, az ezen modulok megvalósítási problémáival nem neki kell megküzdenie, azt már azok tervezői és gyártói megtették.

Ez olyannyira értékes szempont, hogy épeszű keretek között az anyagtakarékosságot és a méret-optimalizálást is felülbírálja: Ha valahová a méretezés szerint minimum 11.712 mm vastag csavar kellene, akkor nem esztergáltatunk pontosan akkorát pontosan a szükséges darabszámban, hanem betervezünk 12-est, vagy rátartással akár 14-est, amiből fél óra alatt lehet venni kilószámra fillérekért bárhol.

 

Iparági előírások

Egy villamosmérnöki tervezésnél konkrét előírás van arra, hogy milyen felhasználásra milyen fajta vezetékek használhatóak, azokból milyen terhelésre milyen minimum vastagság van előírva, és milyen környezetben milyen szigetelési és érintésvédelmi intézkedéseket kötelező megtenni.

Egy épületvillamossági terv nem kap jóváhagyást, ha ezen előírásoknak nem felel meg, és jó okkal: ez garantálja az üzembiztonság első, legalapvetőbb szintjét (amire aztán épülhet a többi).

Egyrészt tehát ezek számítgatásával sem kell bajlódni (és óhatatlanul elrontani egyet az ezer, vagy akár csak a tízezer esetből), valamint ha más munkájához csatlakozik a miénk, akkor építhetünk arra, hogy ő is ezen elvek szerint dolgozott.

 

Szabályozott folyamatok

Ahhoz, hogy egy termék forgalomba kerülhessen, pontos specifikációt kell adni arról ('mit vállal?'), a felhasználási paramétereiről ('milyen körülmények között vállalja?'), a gyártás mérőszámairól és a folyamat ellenőrzési pontjairól ('honnan tudhatjuk, hogy tényleg teljesíti a vállalást?'), és szükség van a teljes felépítési és működési leírásra is ('hogyan teljesíti a vállalását?').

Azt, hogy közben más problémákat nem okoz, azt nem kell külön igazolni, mert azt az iparági előírások betartása hivatott garantálni.

Az emögötti koncepció az, hogy aki a mi termékünket használja építőelemnek az ő termékéhez, az ugyanúgy számolhasson a mi vállalásunkkal, mint ahogy mi is számoltunk a mi építőelemeinkével.

 

Összefoglalva

A mérnök munkája nem izgalmas és újdonságokkal teli, hanem 'csak' a derék, jól végzett tevékenység megelégedettségét nyújtja: a mérnök jól bevált, kipróbált, megbízható és ellenőrzött elemekből és technológiákból építkezve biztosít a feladatra stabilan alkalmas, kipróbálható, megbízható, ellenőrizhető megoldást.

Ez nem a kreativitás hiányát jelenti, hanem inkább olyan szabályok betartását és olyan hozzáállással végzett alkotást, ami terméket eredményez, azaz valami olyasmit, amit nem-mérnöki felhasználó is tud a megadott célok elérésére használni.

Nem magunknak tervezünk, nem a világ szépségére, nem a tudásunk csillogtatására, hanem egyes egyedül azért, hogy a megrendelő igényeire hatékony és fenntartható megoldást  nyújtsunk.

Az ilyen, terméket előállító tevékenységet hívjuk iparnak.

 

A kutató / fejlesztő / konstruktőr tevékenysége

Ők azok, akik  új, soha nem látott dolgokat találnak fel vagy fejlesztenek ki, akik olyasmiket hoznak létre, amikre más még nem is gondolt.

Ez persze teljesen más megközelítést igényel, mást nyújt és mások a lehetőségek, és mások a problémák is.

 

Nincsenek keretek

Feltalálni valamit kétségtelenül izgalmasabb a mérnöki munkánál, és kötetlenebb is, hiszen új területről lévén szó, még nincsenek előírások, nincsen 'keret', amihez igazodni kellene.

Van, hogy egy prototípus elektromos kapcsolásnál még az érintésvédelem is csak minimálisan van megoldva, hiszen a dolog kísérleti jellegéből nem kell olyasmikkel foglalkozni, hogy pl. mi történik, ha a berendezést kinti hidegből meleg helyiségbe viszik, és így a burkolatán pára csapódik ki.

Az érem másik oldala, hogy olyasmi sincs, amihez igazodni, amire építeni lehetne, tehát a konstruktőr csak magára számíthat, hiszen olyasmikre használ mindent, amire annak tervezői nem gondolhattak, neki semmi se garantált, minden előfordulhat.

Az első, még kísérleti turbinás hajtóműnél sem lehetett előre tudni, hogy hogyan fog viselkedni a tengely anyaga a konstans magas hőmérsékleten a 20 kHz körüli mechanikai rezgés hatására, mert ilyennek azelőtt még nem volt anyag kitéve.

 

Nincs termék sem

Ennek megfelelően viszont az így előállított dolgok, még amikor működnek is, akkor sem tekinthetők terméknek, hiszen nem ismeretesek a korlátaik, nem lehet rájuk érdemi garanciát vállalni, így tiszta lelkiismerettel nem lehetne a felhasználóknak kiadni őket. Nem beszélhetünk tehát közvetlen haszonról sem, nincs megtérülés sem, optimalizálni sincsen mire.

Szigorúan nézve a kutatás-fejlesztés (K+F) egy pénznyelő, amit bizonyos intézmények azért tartanak fenn mégis, hogy az így előálló ismeretekből később, hosszabb távon lehessen gyümölcsözőbb technológia.

A félvezető lézerek tömegtermelése előtt az optikai adattárolás is csak egy érdekes laboratóriumi kísérlet volt, amihez rezgésmentes környezet és drága kísérleti eszközpark kellett; a lehetőséget igazolta ugyan, de mindennemű gyakorlati haszon nélkül. Az majd később jött, amikor már kialakult a lézerdiódák olcsó gyártástechnológiája, amihez viszont pont az optikai adattárolás bizonyított lehetősége adta meg az indíttatást.

 

Más tehát a cél

A cél tehát általában valamely elv vagy konstrukció működőképességenek az igazolása, valami nagyon korlátozott feltételek között ugyan, de működő prototípus előállítása, amiből későbbiekben sok-sok munkával ipari technológia is válhat.

Bár a kutatás által létrehozott működő prototípus tulajdonképpen nem a cél, hanem igazából csak a rajt: abból, ami valamilyen körülmények között egyszer működött, pontosan meg kell határozni a szükséges feltételeket és az elvárható működés paramétereit, és megbízható, reprodukálható, a belelátás és belenyúlás igénye nélkül felhasználható építőelemet, technológiát kell belőle csinálni, ami a fejlesztés feladata.

 

Összefoglalva

A konstruktőr egy sejtésből új technológiát állít elő, vajmi keveset törődve közben rend- és módszerességgel, kiszámíthatósággal, de ez szükséges is ahhoz, hogy az eddigieken túlmutató, új dolgok jöhessenek létre.

Azon dolgozunk, hogy megmutassuk, hogy a dolog megoldható, működésre bírható, hogy lehetséges. Nem valami konkrét célért, hanem az új lehetőségek feltárásáért.

Az ilyen, lehetőségeket feltáró tevékenységeket hívjuk kutatás-fejlesztésnek.

 

A kontár 'Mekk mester' tevékenysége

És persze vannak azok, akik ötvözni próbálják a mérnöki munka megoldás-központúságát a feltalálói munka kötetlenségével, és önfeledten gányolnak bármit és mindent a megrendelőnek, tekintet nélkül annak a biztonságára vagy fenntarthatóságára.

Sokszor pont ezeknek a félmegoldásoknak az előnyös tulajdonságait ('olcsó!', 'kicsi!') használják reklámként, az ezért beáldozott egyéb tulajdonságok említése nélkül, ezzel letarolva a keresletet a korrekt(ebb) megvalósítás elől.

A kontárság kicsiben azzal kezdődik, hogy az illető a réz burkolatot vascsavarral rögzíti (olcsóbb, csak fokozott korróziót okoz), nagyobb léptékben azzal folytatódik, hogy egy kapcsolóüzemű tápegységből kispórolják a szűrést és a túlfeszültségvédelmet (olcsóbb, csak hajlamos leégetni a táplált berendezést), és kicsúcsosodni a sajtóból ismeretes autóipari botrányok környékén szokott.

 

Összefoglalva

Amikor valaki az igénytelenség, a hozzáértés hiánya vagy a rövidtávú haszon növelése miatt indokolatlanul tökéletlen, csökkent működőképességű vagy minőségű kísérleti vagy fejlesztési szintű dolgokat kész termékként tálal, akkor az nem termék, hanem megtévesztő selejt.

Hál'Istennek hivatalos kereskedelmi forgalomba csak nagyon korlátozottan jut tovább az ilyesmi, köszönhetően a kötelező norma-kontrollnak, legalábbis a világnak ezen a részén, úgyhogy az ide tartozó dolgokat inkább a kisipari mókolásnak és a meggondolatlan dzsunka-importnak köszönhetjük.
Nem szabad viszont lebecsülni a sok apró kókányolás hatalmát: ha egy piac 99%-át ilyen teszi ki, akkor aközött elvész az 1% normális, és ez válik normává.

Az ilyen, kontár selejtet termelő tevékenységet pedig egyéb iránt gányolásnak hívjuk :).

 

Az IT szektor jelen állapota

A hosszas felvezetés során meghatározott fogalomrendszerrel tehát hová sorolnánk azt, amit az IT terén manapság látunk? Haladjunk inkább sorban, nézzük először az objektív tényeket.

 

Szabványok

Nagyon kevés a szabványosított dolog. A TCP/IP például az, és nincs is vele gond. A HTML például nem az, és pont annyira lehet is építeni a kezelésének a mikéntjére.

A szabványok velejárói, hogy pontosan leírják az általuk meghatározott dolog mérési pontjait és az azokra megfogalmazott előírást, és ennek megfelelően egy TCP/IP stackről el lehet dönteni, hogy úgy viselkedik-e, ill. hogy megbízható-e, vagy sem.
Az újabb keletű dolgok, amiknek a specifikációja (ha van) erősen használja a 'might/may/shall/should' fogalmakat, azok nem szabványok, egy ilyesmi olyan helyen tud meglepetést okozni, ahol nem is várná az ember.

Meglepően hatékony volt annak idején viszont pl. a JavaEE terén a referencia-implementációval történő specifikálás (glassfish): ami azzal működött, az szabványos, ami nem, az nem.

Lehet, hogy nem a legjobb megoldást fogalmazta meg, de teljesen egyértelmű volt, tehát lehetett rá építeni. 

Iparági kontroll

Nincsen. Konkrétan semmilyen nincsen.

Forgalomba kerülő autót csak úgy tervezhetek, ha megvan a szükséges végzettségem, ha a terv átmegy számtalan ellenőrzésen, és azután a null-széria is kiállja a tényleges törésteszteket.
Szoftvert ezzel szemben bárki fejleszthet, publikálhat és hozhat forgalomba, és a készterméknek sem kell a legkisebb teszten se megfelelnie.

Nem hivatalos irányelvek vannak (SOLID, CleanCode, stb.), amiket általában csak akkor szokás megszegni, ha valakinek épp úgy jön kézre, és perse erre vonatkozóan sincs semminemű ellenőrzés.

Ez különösen azért problémás, mert hiába tartaná magát ehhez valaki, ha az általa használt építőelemek megbízhatatlanok: az eredmény is az lesz.

 

Termék-szemlélet

Szintén hiánycikk. Ritka az olyasmi, amikor egy program teljes és használható dokumentációval érkezik, egyrészt mert a szabványok hiánya miatt nehéz is lenne megfogalmazni pl. azt, hogy mit is értünk a 'HTML-t jelenít meg' alatt, másrészt pedig általános érvként használatos, hogy 'ott a forrás, ha érdekel, nézz bele'.

Most képzeljük el, hogy veszünk egy autót, a leírása egy reklámprospektus, és amikor megkérdezzük, hogy milyen üzemanyag kell bele (oktánszámot is beleértve!), akkor azt kapjuk válaszként, hogy 'hát szedd szét a motort, aztán olvasd ki belőle, hogy mit mennyire tolerálna'...



Kutatás-fejlesztés?

Igen, sajnos a termékek nagy része (a pénzeseké is!) erősen prototípusnak mutatkozik. Működik valahogyan, valamikor, de nincs is célba véve, hogy ez vagy általánosabb legyen, vagy legalább ismerjük meg a peremfeltételeit.

A nyílt forráskódú világ pedig szinte teljesen ilyen, befejezett, vagy legalábbis funkcionálisan teljes verziójú dolgot nem találni, a dokumentáció és support pedig ritka, mint a fehér holló.

És ezzel így még semmi baj sem lenne!
Mármint ha emellett lenne szoftver-termék és -ipar is, azaz a forradalmian ötletes barkácsvívmányok mellett lennének alternatív lehetőségként konzervatív, bejáratott, kevesebbet ámde azt stabilabban tudó programok is.

 

Gányolás?

Hajjaj. Fejlesztő lehet bárki, aki gépelni tud, és be tudja copy-paste-elni a 'git commit; git push' parancsokat...
Árulkodó, hogy a pl. stackoverflow-n feltett kérdésekre a válaszok hány százaléka követi a 'hát biztosan nem tudom, de nekem így működött'-sémát.

El lehet képzelni, hogy milyen minőség és megbízhatóság kerül ki egy olyan fejlesztő keze alól, aki nem ismeri az általa használt eszköztárnak még az általa használt részét sem. Nos, pontosan olyan...

 

A felhasználói kultúra

Meglepő, de nincsenek elvárások! Ezen a téren teljesen általános és elfogadott az igénytelenség, értve ez alatt az igényektől való mentességet, azaz a felhasználók teljesen normálisnak tekintik, hogy
  • egy program lefagy
  • újra kell indítgatni
  • nem tudni, hogy mit csinál (mit hová továbbít)
  • adatokat ad ki kívülállóknak
  • a felhasználó tudta és engedélye nélkül változásokat eszközöl
Ugyanez tapasztalható az információkkal kapcsolatban is. Nem lepődik meg senki, ha
  • egy weboldalon nem az van, mint amit a címe mond
  • egy kereső olyan találatokat ad ki, amiken nincs is rajta a keresett dolog
  • kéretlen reklámokkal bombázzák az embert
  • a kapott információ pontatlan, idejétmúlt, és nincs felelőse (se dátum, se forráshivatkozás)

Közben az általános panaszkodás ellenére mindenki továbbra is használja a kifogásolt programokat/szolgáltatásokat/forrásokat, amikor pedig pl. háztartási gépek kapcsán hasonló esetben nem hogy kidobná azt, de attól a gyártótól mást se venne még egyszer, még akkor sem, ha nincs igazi alternatívája.
Megszoktuk, elfogadtuk, és már teljesen természetesnek vesszük, hogy a szoftver megbízhatatlan és az adat hamis, és nem vagyunk hajlandóak még arra sem, hogy 'a pénztárcánkkal szavazzunk'.

Ahogy a szoftver-világban régen használatos 'stable/unstable/testing' felosztás kihalt, úgy vette át a helyét az 'örök béta' hozzáállás.

Ha az autónktól tapasztalnánk ehhez hasonló üzembiztonságot, akkor attól a gyártótól lábtörlőt se vennénk többé, azt viszont szó nélkül lenyeljük, hogy a telefonunkról azt sem tudjuk, hogy mi miatt használt el mennyi adatforgalmat ill. akkumulátor-töltést.

Szó nélkül töltjük le hetente az újdonságot nem hozó frissítéseket, amikre csak azért van szükségünk, mert a programok/weboldalak igénylik, amik viszont csak azért igénylik, mert már mindenki frissített úgyis, és már ez az aktuális.

Úgy teszünk, mintha a szoftver ingyen volna, és ezért elvárásaink se lehetnének vele szemben, pedig még ha a bekerülési költségét tekintve ingyenes is, a használati költségét tekintve nem az, mert a pénzünkkel fizetünk a sávszélességért, az akkutöltésért, a megbízhatatlanság miatt szükséges mentésekért, az adatvesztés okozta kárért, az időnkkel az összes járulékos (azaz nem a célra irányuló) ráfordított kattintásért, a figyelmünkkel az arcunkba tolt összes spamért és reklámért. Jócskán van ára a szoftvernek (különben nem lehetne belőle megélni), viszont mégis úgy fogadjuk, mintha ajándékba kaptuk volna...

 

Összefoglalva

Az alkalmazói szoftverek terén tehát nem igazán beszélhetünk termékről, tehát iparról sem, és ennek megfelelően mérnökről is csak elég ritkán.

Túlnyomórészt a jóindulatra épülő ad-hoc fejlesztgetés és többé-kevésbé jóra törekvő, ámde kontrollálatlan háztáji munka zajlik, aminek pontosan annyi köze van az iparhoz, mint egy favelában épülő falaknak és házaknak az építészethez és az épületgépészethez.

Ennek pedig a legfőbb, a kontroll hiányán is túlmutató oka a norma hiánya: a túlnyomó többségnek nincs is igénye ennél jobbra.

Mivel bizonytalan alapra stabil dolgot építeni nem lehet, ez a trend így fog sajnos folytatódni is. Kiváltani ezt legfeljebb egy igényes hozzáállásra alapuló szoftver-kultúrával lehetne, viszont annak már induláskor kellene mindazt tudnia, amit a mostani instabil info-világ úgy-ahogy, de tud, ezt pedig egy lépésben lehetetlen megtenni.

Tehát a 'szoftvermérnök' és '-ipar' alá továbbra is odaértjük ezt a színvonalat is, a hagyományos, vagy most már mondhatjuk azt is, hogy a valódi mérnökség és ipar fogalmát is devalválva ezzel.

"Valahol egy fa keményen dolgozva állítja elő azt az oxigént, amit mi most erre elpazaroltunk. Ideje volna lassan megkeresni, és legalább megköszönni neki az erőfeszítést..."

Sunday, November 20, 2016

Java - nyelvi alapok

Futtatni vagy fejleszteni?

  • JRE = Java Runtime Environment, csak futtatni
  • JDK = Java Development Kit, fejleszteni is, de ebben van egy JRE is

Fejleszteni, de milyen környezetben?

Rengeteg IDE-s fejlesztőeszköz van a világon, NetBeans, Eclipse, IntelliJ, Anjuta (és még az AndroidStudio is ide tartozik), amik számos hasznos dologgal segítenek minket, de közös nevezőként akár egy sima szövegszerkesztővel és a parancssoros fordítóval és futtatóval is el lehet boldogulni.
Pláne nagyobb és összetettebb rendszereknél hasznosak az IDE-k, a választás ízlés dolga, de az itteni példák bárhol működni fognak.

A futtatási koncepció

A Java fordított nyelv (szemben az értelmezett nyelvekkel), azaz a forráskódból egy bináris program fog készülni, és ezt lehet majd végrehajtani. Azért viszont, hogy ez különböző platformok között hordozható legyen, nem valamely architektúra gépi kódjára fordul le, hanem egy célspecifikus virtuális gépére (JVM - Java Virtual Machine), és ennek a virtuális gépnek van kvázi emulátora majd' minden architektúrához.
Maga a JVM egyébként kvázi szabvánnyá nőtte ki magát, olyannyira, hogy vannak egyéb nyelvek is, amik JVM binárisra fordulnak, annak ellenére, hogy közük sincs a Javához :D.
A JRE tulajdonképpen a JVM-et tartalmazza, és emellett még a nyelvhez adott támogató könyvtárat (ez tud olyasmiket, mint pl. file-kezelés, adatszerkezetek, stb.), meg a dokumentációt.
A forrásprogramok hagyományos kiterjesztése a .java, a lefordított binárisoké a .class, illetve hogy könnyebb legyen az összetartozó .class-ainkat (package) együtt kezelni, ezeket .jar (Java Archive) -okba tudjuk összetömöríteni.
A .jar amúgy technikailag .zip, csak a .class-okon kívül egyéb meta-adatokat is tartalmaz, mint pl. a package neve, verziója, opcionálisan digitális aláírás, stb.
(Lesz még egy fajta file, a .properties, ami 'kulcs: érték' formában utólagosan a user által módosítható beállításokat tartalmazhat, és amihez a standard library jó támogatást nyújt, így nem kell nekünk saját config-file-kezelést megírnunk. De erről majd a maga idejében.)

A fejlesztési koncepció

Az az alapgondolat, hogy maga a Java csak egy nyelv, bizonyos tulajdonságokkal és képességekkel, de a beépített nyelvi elemek minden specifikusságtól mentesek legyenek,
illetve ehhez legyenek gazdag és sokrétű, előre megírt osztály-könyvtárak, amik tartalmaznak rengeteg olyan dolgot, ami a sima nyelvi elemekkel megvalósítható, és amire általában szükség szokott lenni.
Tehát pl. az elemi típusok (karakter, egész, logikai, stb.), az adat-konstrukciók (tömb, struktúra/osztály), a programkonstrukciók (elágazások, ciklusok, osztályok koncepciója) azok nyelvi elemek, de a String típus, a File, a List és ilyesmik már külső osztálykönyvtárakból jönnek.
Elvileg ezek nélkül, pusztán a nyelvi elemekkel is lehetne dolgozni, csak kár volna újraírni és újratesztelni mindent :D
Az osztálykönyvtárak hierarchikusan vannak szervezve, ahol az egyes szintek neveit ponttal választjuk el, azaz pl. a 'java.*' az alap Java osztálykönyvtár, ezen belül a 'java.io.*' a be/kimenettel kapcsolatos dolgok helye, a 'java.util.*' a mindenfél segéd-cuccé, az 'org.apache.http.*' az Apache által adott osztálykönyvtárban a HTTP protokollal kapcsolatos dologké, és így tovább.
Az ütközések elkerülése végett a 'java.*' és a 'javax.*' a JRE-ben gyárilag benn lévő osztálykönyvárak, mindeni más pedig legyen szíves a megfordított domain nevével kezdeni a könyvtárainak a neveit (azaz 'apache.org' -> 'org.apache.*').

Melyik verzióban mi minden van?

  • Java SE = Standard Edition, a nyelv maga, meg a standard osztály-könyvtár
  • Java ME = Micro Edition, kb. ami nem kötődik GUI-hoz, ill. a szűk minimum
  • Java EE = Enterprise Edition, web services, servlet, jsp, ejb, persistence, transaction, minden, ami csak van
Mi most a Java SE-vel fogunk kezdeni, a nyelv bemutatásához az pont alkalmas lesz.

A JDK telepítése

Ami tehát nekünk most kelleni fog mindenképpen: Java SE Downloads / JDK

Települ pl. c:\Program Files\Java\jdk1.8.0_111\ alá, a benne lévő fejlesztőeszközök az ez alatti bin\-ben lesznek, a JRE pedig az ez alatti jre\-be kerül.
Tehát a PATH-hoz érdemes (pontosvesszőkkel elválasztva) hozzáadni ezt a c:\Program Files\Java\jdk1.8.0_111\bin-t és a c:\Program Files\Java\jdk1.8.0_111\jre\bin-t.
Ezt ellenőrizhetjük is:
C:\>java -version
java version "1.8.0_111"
Java(TM) SE Runtime Environment (build 1.8.0_111-b14)
Java HotSpot(TM) Client VM (build 25.111-b14, mixed mode)

C:\>javac -version
javac 1.8.0_111

C:\>jar
Usage: jar {ctxui}[vfmn0PMe] [jar-file] [manifest-file] [entry-point] [-C dir] files ...
Options:
-c create new archive
...

Ezzel a három leg-alapvetőbb parancsot is megemlítettük:
  • A 'java' a JVM futtató parancsa, amivel a .class file-okat fogjuk végrehajtani
  • A 'javac' a fordító, ami a .java forrásokból .class binárisokat készít
  • A 'jar' a csomagoló, amivel .class binárisokat foghatunk össze .jar archívumokká

Az első program

Csináljunk egy próba-könyvtárat és tegyük le bele az első teszt-programunkat HelloWorld.java néven:
class HelloWorld
{
    HelloWorld(String[] args) {
        System.out.println("Hello World!");
        for (String s : args) {
            System.out.println("arg: " + s);
        }
    }

    public static void main(String[] args) {
        new HelloWorld(args);
    }
}
Ezt fordítsuk is le, és futtassuk:
C:\Temp\dev_java>javac HelloWorld.java
C:\Temp\dev_java>java HelloWorld alma korte szilva
Hello World!
arg: alma
arg: korte
arg: szilva

Nos, valamit már csinál, viszont ahhoz, hogy el tudjuk mondani, hogy mi mit csinál és miért úgy, ahhoz először pár nyelvi koncepciót meg kell néznünk, úgyhogy ezt most tegyük félre egy kicsit.

Nyelvi koncepciók

Mindenek előtt, a Javában minden, kívülről elérhető típusnak saját file-ban kell helyet kapnia, és a file elérési útvonala 1-1 viszonyban van a típus nevével, azaz a 'HelloWorld' típusnak a 'HelloWorld.java'-ban van a helye.
Emellett még lehetnek egyéb segéd-típusok is ebben a file-ban, de azok kívülről nem lesznek elérhetőek.

Elemi típusok és operátorok

A Java struktúrált, és azon belül objektum-orientált nyelv, erősen típusos, tehát az adatokat valamilyen típusú változókban tarthatjuk, ezekből adat-konstrukciókat építhetünk fel, a végrehajtás pedig elemi utasításokból épül fel, amikből program-konstrukciókat szervezhetünk. (Tehát nem pl. SQL vagy Prolog :D...)

A nyelv minden eleme kis/nagybetű-érzékeny, azaz az 'ize', az 'IZE' és az 'Ize' három különböző dolog. Szokás a változókat kisbetűvel kezdeni, az összetett típusok neveit naggyal, a konstansokat pedig végig naggyal írni, így már ránézésre is láthatjuk, hogy mi micsoda.

Az elemi típusok:
  • byte (1 byte-os előjeles egész, -128..127), pl. -42, 0x2a,
  • short (2 byte-os, -32768..32767)
  • int (4 byte-os, -2G ..2G - 1)
  • long (8 byte-os, -sok .. sok -1)
  • float (4 byte-os valós, kb. 7 jegy pontossággal), pl. 3.1415f, 6.022e23
  • double (8 byte-os valós, kb. 16 jegy pontossággal), pl. 3.1415, 1.38e-23
  • boolean (logikai), pl. true, false
  • char (unicode karakter), pl. 'а' , 'Я', '\n' (sorvégjel) '\u263a' (smiley)
És mi a helyzet a stringekkel, ha már a példában használtuk is?
Nos, a 'String' az összetett típus, a rendes polgári neve amúgy 'java.lang.String', ami azt jelenti, hogy ő a 'java.lang' osztálygyűjteményben lakik, és ebből azt is láthatjuk, hogy ezen gyűjtemény annyiban speciális, hogy az ő típusaira röviden is hivatkozhatunk.

Visszatérve, amikor egy adott típusból változót szeretnénk megadni, akkor a kb. a C szintaxis szerint történik:
int x, y, sum; // ez itt komment a sor végéig
boolean isValid, containsSpace; /* ez pedig a bezárásáig */
Tehát a változó-deklaráció a 'típusnév var1, var2, ..., varN;' formában történik, a változónevek pedig nem kezdődhetnek számmal, nem tartalmazhatnak szóközt vagy valamely operátor-karaktert, nem egyezhetnek nyelvi kulcsszavakkal, egyébként bármit tartalmazhatnak, akár arab karaktereket is.

Kommenteket a forráskódba a '//'-től sor végéig ill. '/*'-tól '*/'-ig terjedő formával tehetünk - pont mint C++-ban.

Szokás a változókat kisbetűvel kezdeni és beszédes neveket adni nekik, amelyeknél az összetett neveket camel case-szerűen tagoljuk, azaz az első betű kicsi, a többi szókezdő viszont nagy. (Elsőre furcsa, de egy idő után rááll a szem...)

Az egyes elemi típusokból készíthetünk tömböket, amik viszont külön típusnak számítanak,
azaz
float[] sampleWeight;

Fontos, hogy ezzel -az elemi típusú változókkal szemben- még csak azt mondtuk meg, hogy a 'sampleWeight' egy valós tömb lesz, de még nem hoztuk azt létre.
sampleWeight = new float[23];
sampleWeight[0] = 11.1;
sampleWeight[1] = sampleWeight[5] = 2.4;
sampleWeight[2] = (sampleWeight[0] + sampleWeight[5]) / 2.0f;
isValid = (sampleWeight[2] > 1.0f) && !(sampleWeigth[0] <= 100.0f);

Ebből látjuk, hogy új tömböt (meg majd új bármit is) létrehozni a 'new típusnév' formában tudunk, a tömb-elemekre pedig a szokott formában hivatkozhatunk.

Az értékadást eszerint a sima '=' jelöli, az aritmetikai operátorok között a szokásos +, -, *, / (egész osztás), % (maradék) működik, az összehasonlító operátororok == (egyenlő-e), != (nem egyenlő-e), <, >, <=, >=, a logikai operátorok ! (tagadás), && (és), || (vagy), és a zárójelezéssel lehet a precedenciát felülbírálni.

Működik a C-beli láncolt értékadás, azaz a 'v1 = v2' értékadásnak mint kifejezésnek is van értéke, nevezetesen a v2.

A bit-szintű műveletek szintén a C szerintiek: & (bináris és), | (bináris vagy), ^ (bináris kizáró vagy), ~ (bináris invertálás), << (balra shift), >> (jobbra shift), illetve egy újdonság: >>> (előjel nélküli jobbra shift).

Működik egyébként a C-ből ismerős prefix és postfix növelés és csökkentés:
  • 'változó++': növeld a változót eggyel, a kifejezés értéke a növelés előtti érték
  • '++változó': növeld a változót eggyel, a kifejezés értéke a növelés utáni érték
  • 'változó--': csökkentsd a változót eggyel, a kifejezés értéke a csökkentés előtti érték
  • '--változó': csökkentsd a változót eggyel, a kifejezés értéke a csökkentés utáni érték
Hasonlóképpen működnek a műveletes értékadások:
  • 'v1 += v2': v1 = v1 + v2
  • 'v1 -= v2': v1 = v1 - v2
  • stb.
És az egyetlen három operandusú művelet:
  • 'isValid ? x : y'
    Ha az isValid igaz, akkor a kifejezés értéke x, különben y
Egyébiránt a 'new' is operátor: a 'new típusnév' értéke egy, az adott típusból létrehozott új példány :D. Lesz még 4 db operátor (metódushívás, attribútum-elérés, típus-ellenőrzés és típus-konverzió), azokról viszont később esik majd szó.

Blokkok

A C-ből ismert módon a { }-ek közötti rész egységnek számít, azaz mindenhol állhat, ahol egyedi utasítás állhat, és a tartalma szépen szekvenciálisan hajtódik végre (kivéve...)
Az egyetlen említésre méltó dolog az, hogy a blokkokban bárhol szintén lehet változókat deklarálni, amik a blokkból kilépéskor automatikusan megsemmisülnek. Ha egy blokkbeli változónak ugyanaz a neve, mint egy blokkon kívülinek, akkor a belső 'elfedi' a külsőt.

Vezérlési struktúrák

Egy az egyben a C mintájára zajlik:

Feltételes végrehajtás

if (logikai_kifejezés)
    ha_igaz_akkor_teendő;

if (logikai_kifejezés)
    ha_igaz_akkor_teendő;
else
    különben_teendő;

Érték szerinti elágazás

switch (egész_vagy_enum_kifejezés) {
    case érték1:
        teendő1;
        break;

    case érték2:
        teendő2; // szándékos fallthrough
    
    default:
        különben_teendő;
}

Feltétel-vezérelt ciklusok

while (logikai_kifejezés)
    amíg_igaz_teendő;

do {
    amíg_igaz_de_legalább_egyszer_teendő
} while (logikai_kifejezés);

C-szerű ciklus

for (kezdeti_művelet; bennmaradási_feltétel; léptetés)
    teendő;

for (int i = 1; i < 2048; i = 2*i)
    System.out.println(i);

Iteráló ciklus

for (változó: tömb_vagy_collection)
    teendő;

for (String s: args)
    System.out.println("arg: " + s);

Visszatérés függvényből/metódusból

return; // void metódusból
return visszatérési_érték; // kifejezés-jellegű metódusból

Kilépés for/while ciklusból

while (...) {
    ...
    break;
    ...
}

kulso_cimke: while (...) {
    while (....) {
        ...
        break kulso_cimke;
        ...
    }
}

For/while ciklus folytatása

while (...) {
    ...
    continue;
    ...
}

kulso_cimke: while (...) {
    while (....) {
        ...
        continue kulso_cimke;
        ...
    }
}

Kivétel-kezelés

Technikailag ez is ide tartozna, de logikailag sokkal később, mert kell hozzá pár egyéb koncepció ismerete.

Osztályok és objektumok

A tömböt már néztük, a másik nevezetes típus-konstrukciót, a struktúrát itt osztálynak hívják, mert pusztán struktúra a Javában nincsen.
class Person
{
    String givenName, familyName;
    int height, weight;
}
Ezzel definiáltunk egy új összetett típust, amiből példányokat szintén a 'new'-val hozhatunk létre:
Person jane, john;
jane = new Person();
jane.givenName = "Jane";
jane.height = 165;
john = new Person();
john.givenName = "John";
john.height = 176;

Mint láthatjuk, ez a 'new' egy kicsit másabb a tömbös new-nál, itt () zárójelpár van mögötte.
Hasonlóképpen az is kiderült, hogy az objektumpéldányok attribútumaira a 'változó.attribútum' formában hivatkozhatunk - ez a '.' a már említett 4 maradék operátor egyike :D.

Komoly probléma persze, hogy így az objektum adatainak a kezelését, beleértve az inicializálást is, a hívóra bíztuk, ami nem túl szerencsés.
Az osztály és a struktúra egyik leglényegesebb különbsége, hogy a struktúra csak összetartozó adatokat fog egybe, az osztály viszont az ezeket kezelő kódot is tartalmazza.
Például amíg egy String példány (ami maga is egy objektum) nincs inicializálva, addig egy speciális értéket a 'null'-t tartalmaz, és bármit próbálnánk kezdeni vele, az futási hibát eredményezne. Ha viszont a hívóra bízzuk az adatmezők kezelését, akkor semmi garancia, hogy ez nem következik be, például a fenti kódban a példányok 'givenName' mezői kaptak értéket, de a 'familyName'-ek nem.
Ezt azzal tudjuk megelőzni, ha megadjuk azt a kódot, amivel ezen típus egy-egy új példányát inicializálni kell: a konstruktort.
class Person
{
    String givenName, familyName;
    int height, weight;

    public Person() {
        givenName = familyName = "N/A";
        height = weight = 0;
    }
}
Így amikor valaki leírja, hogy 'x = new Person()', akkor a fenti Person.Person() fog lefutni, és az korrektül inicializál mindent.
Mint láthatjuk, az osztály attribútumaira a saját metódusaiban csak szimplán az attribútumnévvel hivatkozhatunk, ez az épp aktuális példány attribútumát jelenti.
Magára az aktuális példányra a 'this' kulcsszóval hivatkozhatunk, tehát pl. írhattuk volna azt is, hogy 'this.givenName = ...'.
Persze nem csak az inicializálás az egyetlen művelet, amit megadhatunk:
class Person
{
    // ...
    public String toString() {
        return givenName + " " + familyName + " (" + height + " cm, " + weight + " kg)";
    }
    // ...
}
// ...
System.out.println(jane.toString());
System.out.println(john);

A fenti 'toString()' egy kicsit mágikus név, abban az értelemben, hogy ha valahová String-re lenne szükség (mint pl. a System.out.println(String) kiírásban), de egy objektum-példányt adtunk meg, akkor az implicite konvertálódik a toString() metódusának segítségével.
Ez kicsit hasonlít ahhoz a mágiához, ahogy Stringeket a '+' jellel össze tudunk fűzni, ez szintén 'syntactic sugar', elvileg ez nem volna helyes kifejezés.
(Természetesen a 'System' is valójában 'java.lang.System', ami ugye szintén egy mágia :D
Sokan felróják az ilyesmit a Java nyelvnek, nem is teljesen alaptalanul...)

Objektumok megszűnése

Konstruktort már írtunk, mi a helyzet az ellenpárjával, a destruktorral? Olyan nincsen!
A Javában minden objektum-példány implicite tartalmaz egy referencia-számlálót, ami nő eggyel, amikor a példány címét egy új változónak értékül adjuk, és csökken, amikor az a változó egy új értéket kap. Például a fenti példában a két Person-példány refcountja 1-1, mert mindegyikre 1 változó (a jane és a john) hivatkozik.
De ha ezt írnánk:
// ...
Person x = jane;
// a 'jane' által hivatkozott példány refcountja itt már 2
Person y = john
// most már a 'john' által hivatkozotté is
x = y;
// a 'jane' által hivatkozotté most megint 1, a 'john'-é pedig 3
jane = null;
// a korábban a 'jane' által hivatkozott példány refcountja már 0!
Azt hiszem, érthető a dolog :D.
Amikor elvesztettük/eldobtuk az utolsó hivatkozást is egy példányra, akkor annak a refcountja 0-ra csökken, és már ha akarnánk, se tudunk többé rá hivatkozni, így ez fölöslegessé vált.
Ilyenkor ez a példány a 'szemétkosárba' kerül, és majd valamikor, amikor a JVM épp ráér, akkor szép komótosan elkezdi őket felszabadítgatni. Például a most megszüntetett Person példánybeli két String-példány refcountjait csökkenti, és mivel azok is 0-ra futottak, szintén mennek a szemétbe.
Persze az élet kissé bonyolultabb, pl. ha a Person-t kiegészítjük így:
class Person
{
    //...
    Person spouse;
}
// ...
Person jane = new Person(...);
Person john = new Person(...);
// mindkét példány refcountja 1-1
jane.spouse = john;
// a 'john' példány refcountja 2, mert ennyien hivatkoznak rá: john és jane.spouse
john.spouse = jane;
// most már a 'jane' példány refcountja is 2
john = jane = null;
// mindkét példány refcountja 1-1 (egykori_john.spouse és egykori_jane.spouse)
Akkor most ezek a végtelenségig életben tartják egymást, vagy mégsem?
A felszabadító mechanizmus elég okos ahhoz, hogy az ilyen köröket felismerje, azaz valójában amihez a főprogramból nem lehet eljutni, azt szabadítja fel, a refcount csak könnyítheti a döntést.
Szóval ha egy példányra már nincs szükségünk, akkor csak nullázzunk ki minden rá mutató referenciát, és a többit a Garbage Collector (GC) majd elintézi.

Paraméterek a metódusoknak és polimorfizmus

Természetesen a hívási paraméterek fogalma itt is megvan, sőt, arra is van módunk, hogy ugyanazon nevű metódust más és más paraméterlistával újra definiáljuk:
// ...
public Person(String gn) {
    givenName = gn;
    familyName = "N/A";
    height = weight = 0;
}
public Person(String givenName, String familyName) {
    this.givenName = givenName;
    this.familyName = familyName;
    height = weight = 0;
}
public Person(Person other) {
    givenName = other.givenName;
    familyName = other.familyName;
    height = other.height;
    weight = other.weight;
}
//...
Person jane = new Person("Jane");
Person john = new Person("John", "Doe");
Person john2 = new Person(john);
A híváskori paraméterlista egyértelműen azonosítja, hogy melyik esetben melyik konstruktort kell hívni. Természetesen ugyanez bármely metódussal ugyanígy működik.
Szokásos trükk, hogy ha valamelyik paraméter és valamelyik attribútum neve ütközik, akkor az önmgában álló név a paramétert jelenti, ott az attribútumra pedig a 'this.valami' formában hivatkozhatunk.
A harmadik konstruktor, ami ugyanezen típus másik példányáról inicializál, az nevezetes, és copy-constructornak hívják, ahogyan a paraméter nélküli konstruktor neve default constructor.

Öröklődés

"Minden bogár rovar, de nem minden rovar bogár" - ismerős?
A példánknál maradva, minden sofőr ember, de nem minden ember sofőr:
class Driver extends Person
{
    String licenseNumber;

    public String toString() {
        return super.toString() + ", lic=" + licenseNumber;
    }

    public Driver(String gn, String sn, String l) {
        super(gn, sn);
        licenseNumber = l;
    }

    public string getLicenseNumber() { return licenseNumber; }
}
// ...
Driver john = new Driver("John", "Doe", "12341234");
Person johnp = john;

john.height = 176;
System.out.println("john=" + john);
System.out.println("johnp=" + johnp);
Ennek az eredménye:
C:\Temp\dev_java>java PersonTest
john=John Doe (176 cm, 0 kg), lic=12341234
johnp=John Doe (176 cm, 0 kg), lic=12341234
Tehát a sofőr is ember, csak neki van jogosítványszáma is, itt Person-t ősosztálynak, a Driver-t leszármazott osztálynak hívjuk, és az erre vonatkozó szintaxis a 'class leszármazott extends ős'.
A Driver-példány inicializálásához három string kell, az első kettő az ősosztály konstruktorához használódik ('super(gn, sn)'), és ehhez nagyon hasonlóan a 'toString()' is felhasználja az ősosztály 'toString()'-jét, meglehetősen hasonló jelölésmóddal.
A 'Person johnp = john' sikeressége mutatja, hogy a Driver egyben Person is, ugyanis az ellenkező irány, pl. egy 'john = johnp' hibaüzenetet adna:
error: incompatible types: Person cannot be converted to Driver
john = johnp;

És egyúttal azt az amúgy fontos dolgot is láthattuk, hogy a metódusok konkrét példányokhoz kötődnek, tehát a System.out.println("johnp=" + johnp)-nél hiába hogy Person típusú johnp-ként hivatkoztunk a Driver-példányunkra, akkor is a Driver.toString() hívódott meg.
Ezt hívják virtuális metódusnak, C++-ban ezt külön kéne jelölni, de Javában minden metódus virtuális.

Szintén fontos, hogy örökölni csak egyetlen típusból lehet, mégpedig azért, hogy megelőzzünk egy ütközést: ha lehetne pl. két ős, és az egyik ős definiál egy 'x' attribútumot 'int'-ként, a másik ős meg 'String'-ként, akkor a leszármazottban milyen típussal szerepeljen, és mi légyen a másik ősnek az ezt piszkáló metódusaival? Nincs jó válasz, tehát nincs többes öröklődés.
Megjegyzendő még, hogy van egy 'Object' nevű típus, ami minden osztály-típusnak implicit őse, azaz mindenki belőle származik, anélkül, hogy ezt explicite kiírná. (Megint egy mágia.)

Az öröklődés jelentése

Az öröklődés konkrétan új információval való bővülést jelent, sem többet, sem kevesebbet.
A valós életben ez helyzettől függően jelenthet akár specializációt, akár generalizációt, úgyhogy a 'mit származtassunk miből' tervezési kérdésnél az 'információval bővülést' kéretik nézni!

Öröklődés mint specializáció

Az Ember típus tartalmaz nevet, életkort, testsúlyt, magasságot, a Sofőr típust pedig az Ember típusból származtathatjuk (a 'jogosítvány száma' lesz a bővülő információ).
Jelentését tekintve a Sofőr pedig az Ember specializációja, mert minden Sofőr egyben Ember is, de nem minden Ember Sofőr.

Öröklődés mint generalizáció

A Négyzet típus tartalmaz oldalhosszot és színt, a Téglalap típust pedig a Négyzet típusból származtathatjuk (a 'másik oldal hossza' lesz a bővülő információ).
Jelentését tekintve a Téglalap pedig a Négyzet generalizációja, mert bár nem minden Téglalap Négyzet, de minden Négyzet egyben Téglalap is.

Típus-ellenőrzés és -konverzió

A fenti példában 'johnp' egy Person-referencia, tehát neki nem tudnánk pl. a licenseNumber-ére hivatkozni, mert a Person-nak olyanja nincsen. Megtehetnénk, hogy áterőltetjük Driver-ré, de ez katasztrófához vezet, ha esetleg mégis csak egy sima Person-ra hivatkozott, ami nem Driver. Tehát először ellenőrizni kellene:
if ((johnp != null) && (johnp instanceof Driver)) {
    Driver johnd = (Driver)johnp;
    System.out.println("lic: " + johnd.licenseNumber);
}
Mint láthatjuk, a 'változó instanceof típus' kifejezést kerestük az ellenőrzés céljára, és a '(típus)változó'-t a típus áterőltetésére.

Absztrakt osztályok

Ha egy ős-osztály valamely metódusát nem valósítjuk meg (mert pl. nem lehet értelmesen), de a leszármazottainak elő akarjuk írni, hogy nekik felül kelljen definiálni, akkor az ilyen osztályt hívjuk absztraktnak:
abstract class Shape
{
    abstract public double getArea();
}
class Circle extends Shape
{
    public double getArea() { return r*r*Math.PI; }
}
Természetesen absztrakt osztályt nem lehet példányosítani, és ha származtatunk belőle, akkor abban vagy minden absztrakt metódust meg kell valósítani, vagy a leszármazott osztálynak is absztraktnak kell lennie.

Láthatóság

A fenti példákban pár helyen előfordult a 'public' szó, és ezt még nem veséztük ki.
Bevezethetünk mi olyan konstruktorokat, amilyeneket csak akarunk, ha a hívónak megengedjük, hogy a példányaink attribútumait kedvére elpiszkálja, pl. 'john.givenName = null'. Ezt csak úgy tudnánk kizárni, ha a védendő atttribútumainkat csak nekünk lenne jogunk állítgatni.
class Person
{
    private String givenName, familyName;
    // ...
Ezután az adott attribútumot csak az osztály saját metódusai tudják elérni, más senki sem.
Ha egy metódust teszünk 'private'-té, akkor értelemszerűen csak az osztály többi metódusa fogja tudni meghívni.
Ehhez hasonló minősítő a 'public', ami mindenkinek elérhetővé teszi az adott attribútumot/metódust, illetve a 'protected', ami csak az osztálynak és a leszármazottainak.
Mi a helyzet azzal, ami se private, se protected, se public? Az az úgynevezett 'package' láthatóság, amikor a velünk egy osztálykönyvtárbeli típusok metódusai érhetik el, más nem.

Statikus metódusok és attribútumok

Ha valami olyan attribútumot vagy metódust szeretnénk létrehozni, ami nem az egyes objektumpéldányokhoz, hanem magához az osztálytípushoz kötődik, arra a 'static' kulcsszó alkalmas.
Ilyenek pl. a konstansok, vagy ha pl. a létrehozott példányokat szeretnénk megszámlálni:
class HtmlColour
{
    public static int RED = 0xff0000;
    public static int GREEN = 0x00ff00;
    public static int BLUE = 0x0000ff;
    private static int numInstances = 0;
    public static int getNumInstances() { return numInstances; }
    public HtmlColour() {
        numInstances++;
        // ...
    }
}
Ezekre példányosítás nélkül is hivatkozhatunk:
int x = HtmlColour.RED;
int y = HtmlColour.getNumInstances();
(Igazából a konstansok deklarációjában szokott még lenni egy 'final' kulcsszó, pl. 'public static final int RED = 0xff0000;', ami annyit tesz, hogy az adott változó/attribútum csak egyetlen egyszer kaphat értéket, onnantól read-only, és így a Java fordító ezzel tud optimalizálni.)

Egy kapcsolódó trükk:
Ha nem szeretnénk, hogy bárki tudja az osztályunkat példányosítani (mert pl. csak egyetlen példánynak van értelme belőle), akkor a konstruktort kívülről meghívhatatlanná tehetjük:
private Driver(...) {
    // ...
}

static Driver theDriver;

public static Driver get() {
    if (theDriver == null)
        theDriver = new Driver(...);
    return theDriver;
}
// ...

Driver x = Driver.get();

Ezt a kód-struktúrát hívja az irodalom 'Singleton pattern'-nek.

Interface-ek

Mint láthattuk, Javában nincs többszörös öröklődés, és jó okkal nincsen. Viszont helyenként igencsak praktikus volna, ha egy általános célú kódban csak valamilyen képességet szeretnénk feltételezni a kapott paraméterről, pl.:
abstract class Readable {
    abstract public char read(); // read one character
}
String readLineFrom(Readable x) {
    String s = "";
    while (true) {
        char c = x.read();
        if (c == '\n')
            return s;
        s = s + c;
    }
}
Ez egy sorvégjellel lezárt sorozatot olvasna be egy Stringbe bármiből, ami a Readable-ből származik és megvalósította az egy-karakter-olvasása műveletet. És ez így még működne is!
A gond akkor jön, ha van hasonló igény pl. egy 'Writable' típusra, és mi valami olyasmit szeretnénk megvalósítani, amit írni és olvasni is lehet (pl. file, hálózati kapcsolat, soros terminál, stb.):
class Terminal extends Readable, Writable { ... }
Terminal t;
String s = readLineFrom(t);
sayHelloTo(t);

Hát ilyen ugye nincsen, pedig az eset életszerű. Erre szolgál a Javának az interface fogalma:
Az interface valamelyest hasonlít az absztrakt osztályokra, csak
  • Az interface-nek nem lehetnek attribútumai, csak metódusai
  • Az összes metódus implicite public, ezt nem kell külön jelölni
  • A metódusokat nem kell külön absztraktnak sem jelölni, elég, ha nincsen törzsük
  • Az interface tartalmazhat statikus metódusokat és konstansokat is
  • Az interface-ből nem leszármazik valaki, hanem megvalósítja azt,értve ez alatt azt, hogy megvalósít minden, az interface-ben megadott metódust
  • Az interface-ben a metódusokhoz a 'default' kulcsszóval adhatunk meg alapértelmezett törzset, ami csak akkor lép érvényre, ha az interface-t megvalósító osztály azt nem valósítaná meg (pl. mert az interface bővült az osztály írása óta)
  • Interface-ek örökölhetnek egymástól
  • Ahol típus állhat, ott interface is, és oda illeszkedik bármilyen típus, ami megvalósítja az adott interface-t.
Tehát pl.:
interface Readable
{
    char read();
}
interface Writable
{
    bool write(char c);
}
class Terminal implements Readable, Writable
{
    public char read() { ... } // from Readable
    public bool write(char c) { ... } // from Writable
    // ...

Generikus típusok

Képzeljünk el egy verem-implementációt, amibe valamilyen típusú objektumokat pakolhatunk be, illetve ugyanezeket vehetjük ki onnak, lesz pl. egy 'void push(...)' meg egy 'Valami pop()' metódusa. No de mi legyen ez a 'Valami'?
Ha én embereket akarok tárolni, akkor Person, ha terminálokat, akkor viszont Terminal. Lehetne a végső közös ős, az Object, de akkor a kivett elemekkel nem könnyen lehetne bármit is kezdeni, amit az Object típus nem tud:
Object x = verem.pop();
x.write('Q'); // hiba: write()-ja pl. a Terminal-nak van, de az Object-nek nincs
Valahogy a listának meg kéne kapnia a típust is, amilyen objektumokat fogadnia és visszaadnia kell:
interface List<T>
{
    void push(T x);
    T pop();
}
List<Person> emberek = new List<Person>();
List<Terminal> terminalok = new List<Terminal>();
emberek.push(john);
Person x = emberek.pop();
A fenti példában a 'T' egy típus vagy interface helyén állt, és a kiértékeléskor az épp aktuális típussal helyettesítődött be.
Természetesen ez nem csak egybetűs lehet, és nem is csak egy lehet belőle, pl. a kulcs-érték párokat tartalmazó java.util.Map szignatúrája 'public interface Map<K, V>', ahol K és V a kulcs és az érték típusai.
Csak konvenció, de pár, egy db nagybetűből álló nevet bizonyos fajta típusok jelölésére használunk:
  • K = Key
  • V = Value
  • N = Number
  • E = Element
  • T = Type (általános)
  • S, U, V, ... = többi (általános) típus

Felsorolási konstans típusok

Ha pl. színeket akarunk megadni, akkor az eddigiek alapján egy típust vezetnénk be, és abban definiálnánk konstansokat (lásd pl. HtmlColour fentebb).
Van viszont erre egy cizelláltabb, erősebb nyelvi lehetőség, az '
enum':
public enum Planet {
    MERCURY (3.303e+23, 2.4397e6),
    VENUS (4.869e+24, 6.0518e6),
    EARTH (5.976e+24, 6.37814e6),
    MARS (6.421e+23, 3.3972e6),
    JUPITER (1.9e+27, 7.1492e7),
    SATURN (5.688e+26, 6.0268e7),
    URANUS (8.686e+25, 2.5559e7),
    NEPTUNE (1.024e+26, 2.4746e7);

    private final double mass; // in kilograms
    private final double radius; // in meters
    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
    }
    private double mass() { return mass; }
    private double radius() { return radius; }
    // ...
Mint láthatjuk, egészen class-ként viselkedik, csak meg lehet adni a lehetséges értékek listáját is.

Package-ek

Láttuk, hogy mindenféle osztálykönyvtárak szép kompakt csomagokban jönnek, és nem ezernyi külön .class file-ban, hát ilyet mi is tudunk csinálni!
Válasszunk egy package nevet, pl. org.dyndns.fules.demo, és hozzunk létre egy ennek megfelelő könyvtárstruktúrát: org/dyndns/fules/demo.
Az egyes osztályainkat (Person, Driver, PersonTest) válogassuk külön-külön file-okba ezen könyvtárstruktúrában (Person.java, Driver.java, PersonTest.java).
Mindegyik ilyen file elején adjuk meg a package nevét, amihez tartozik:
package org.dyndns.fules.demo;
Forgassuk le őket és futtassuk a PersonTest-et:
javac org\dyndns\fules\demo\*.java
java org.dyndns.fules.demo.PersonTest
A .class file-ok a .java-k mellé keletkeztek, tehát még csak félúton vagyunk.
Ezt kissé póriasan, de már összefűzhetjük egy archive-ba, és el is lehet indítani:
jar cvf valami.jar org\dyndns\fules\demo\*.class
java -cp valami.jar org.dyndns.fules.demo.PersonTest
Bár ekkor még kézzel kellett megadni ClassPath-ként (-cp) a .jar-t, és az indítandó osztály nevét, amit be is lehetne ágyazni a .jar-ba:
jar cvfe valami.jar org.dyndns.fules.demo.PersonTest org\dyndns\fules\demo\*.class
java -jar valami.jar
Itt az 'indítandó osztály' annyit jelent, hogy amikor a JVM indul, akkor ennek az osztálynak keresi a 'public static void main(String[] args)' metódusát, amit meghív, paraméterként átadva a kapott parancssori argumentumokat.
Ha kívülről, más osztályokból szeretnénk ezt a csomagot használni, akkor
  • Egyrészt azok fordításánál a '-cp valami.jar' -ral a java .class-ok keresési útvonalához hozzá kell adni
  • Másrészt a használó .java file elején a kívánt osztályt be kell importálni, pl. 'import org.dyndns.fules.demo.Driver;'
Az import-nak van még két fajtája:
  • On-demand: 'import org.dyndns.fules.demo.*;' ekkor minden közvetlen ottani típus beinclude-olódik, amit használni akarunk
  • Static import: 'import static HtmlColours;' ami után a csomagbeli attribútumok is direkt elérhetőek lesznek, azaz nem kell 'HtmlColours.RED', hanem elég csak a 'RED'

Beágyazott osztályok

Eddig minden osztály top-level volt, azaz a package-én belül legkívül, de persze ezt lehet cifrázni is, méghozzá négyféleképpen:

Inner class

Ez az a helyzet, amikor egy osztályon belül definiálunk egy másikat, de az nem static.
Ekkor a belső osztályra csak a külső példányai tudnak hivatkozni, tehát a belső minden példányához tartozik a külsőnek egy példánya, ami létrehozta.
Ezért semmi akadálya, hogy a belső osztály elérje a külsőnek ezen példányát!
class Genus
{
    String genusName; // pl. 'canis'
    class Species {
        String speciesName; // pl. 'aureus'
        class Subspecies {
            String subspecName; // pl. 'indicus'
            public String toString() {
                return genusName + " " + speciesName + " " + subspecName;
            }
        }
        Subspecies[] subspecies;
    }
    Species[] species;
}
// ...
Genus[] genus;
// ...
System.out.println(genus[4].species[1].subspecies[3].toString());
Ha, tegyük fel, mindhárom class csak simán 'name'-nek hívta volna az attribútumát, az sem lenne baj, csak ekkor a toString() így alakulna:
public String toString() {
    return Genus.name + " " + Species.name + " " + name;
}
Az Inner classok természetesen nem tudnak statikus dolgokat deklarálni, merthogy ők mindig a külső osztály valamely példányához kötődnek, a statikus dolog meg attól független volna.
Mivel az interface-ek eredendően példány-függetlenek, ezért Inner interface nincsen :D.

Static nested class

Ha a belső osztály 'static', akkor értelemszerűen nem tud a külső osztálynak a példányhoz kötődő, tehát nem-static dolgaihoz hozzáférni.
Igazából ez olyan, mintha maga is top-level class volna, csak kényelmi/csoportosítási okból került volna egy másik belsejébe.
Erre még kívülről is lehet hivatkozni 'x = new Külső.StaticBelső(...)' formában.

Local class

Az Inner class unokatesója, csak ezt egy tetszőleges { } blokkon belül definiáljuk és használjuk. Egyébiránt ugyanaz pepitában.

Anonymous class

Amikor csak azért kellene egy új classt definiálni, hogy egyetlen helyen egyetlen példányt létrehozzunk belőle, akkor ez megspórolható:
Thread t = new Thread(new Runnable {
    public void run() {
        // csinál valamit egy külön threaden
    }
} );
t.start();
Itt mi tulajdonképpen a Runnable interface-t valósítottuk meg egy névtelen osztállyal, definiálva annak a run() metódusát, ebből hoztunk létre egy új példányt, amit átadtunk egy Thread konstruktorának, majd elindítottuk ezt a threadet.

Exception handling

A Java hibakezelése nem feltétlenül korlátozódik a hibakód-visszadásra, hanem van lehetőség egy kissé rugalmasabb, bár nagyobb odafigyelést igénylő módszerre is.
Amikor valahol a hívási lánc mélyén valami olyasmi történik, ami a további végrehajtást lehetetlenné teszi (pl. hálózati hiba lépett fel), akkor a hiba helyénél mód van a végrehajtást azonnal megtörni, az egymásba ágyazódó blokkokból azonnal kilépkedni (kb. mint ciklusból a break), egészen addig, amíg valaki fel nincs készülve ennek az eseménynek a kezelésére.
Hogy milyen esemény is történt, illetve annak milyen részletei voltak, azt úgy tudjuk feljuttatni ehhez a kezelőhöz, hogy tulajdonképpen egy objektum-példányt 'dobunk' felfelé neki:
throw new NetworkErrorException("füstöl a drót");
Aki pedig ezt el akarja kapni (tetszőleges szinttel feljebb), az így teheti meg:
try {
    // csinál valamit, amiből jöhet a NetworkErrorException
}
catch (NetworkErrorException e) {
    // lekezeli az eseményt
}
catch (FileNotFoundException e) {
    // lekezel egy másik eseményt is
}
finally {
    // ez még akkor is lefut, ha netán valami egyéb exception jött, amit mi is engedünk tovább
    // kiváló hely a cleanup teendőkre
}
Ha egy metódus dobni (vagy továbbengedni) szeretne egyfajta exceptiont, akkor ezt a metódus deklarációjában jelezni kell:
public void Valami() throws NetworkErrorException, IOException { ...
És még egy Jáva-mágia: kivéve, ha az illető exception a RuntimeException-ből származik, mert azt nem kell külön jelezni...

A primitív típusok wrapperei

Ezzel még adósok vagyunk, bár igazából sehová sem passzol rendesen :D.
A primitív típusokat lehet használni kifejezésekben, és ennyi. Nem tudják megmondani az értékkészletüket, nem tudják magukat Stringgé konvertálni (tényleg nem! az mágia volt!), sem Stringből kielemezni, stb., mert nem objektumok.
Hasonlóképpen nem lehet őket generikus típusokban sem használni, tehát nincs 'List<byte>'.
Ezt feloldandó mindegyik kapott egy wrapper classt, ami mindezeket tudja helyette:
Byte, Integer, Long, Boolean, Character, Float, Double.
Nekik van mindenféle szépségük, konstruktor a primitív típusról ill. Stringről, toString(), static valueOf(String s), stb.
Miért van hát akkor szükség a primitív típusokra?
Mert a Javában nincs operator overloading, azaz objektumokat nem tudunk aritmetikai/logikai kifejezésekben szerepeltetni, hozzájuk pl. '+' operátort definiálni.
Ezért kellett, hogy ezek nyelvi elemek legyenek.
És mi is volt az a Stringesítő mágia?
Amikor azt írjuk, hogy
int i = 42;
System.out.println("i értéke " + i);
akkor valójában az int-ről implicite keletkezik egy Integer, és annak hívódik a toString()-je.
Mondom hogy mágia :D !

Static initializer blokkok

Ha valahol, bárhol lerakunk ilyen blokkokat:
static {
    // bármi kód
}
akkor ezek le fognak futni, amikor az adott class betöltődik, méghozzá fentről lefelé sorrendben és pontosan egyszer.
Ronda mágiákat lehet ezzel csinálni, no pláne az enum-ok inicializálásával átfűzve... Ha nem muszáj, ne tegyük!

A leggyakrabban használatos standard package-ek és osztályok

A teljesség igénye nélkül, csak az említés szintjén:
  • java.lang
    Object, String, Math,
    Byte, Integer, stb.,
  • java.util
    List<T>, Deque<T>, Map<K,V>, Vector<T>, Locale
  • java.text
    Format, SimpleDateFormat
  • java.io
    Stream, Reader, Writer, File
  • javax.swing
    GUI komponensek
Mivel ezek már szigorúan véve nem a nyelv elemei, így ezekről egy külön jegyzetben esik majd szó.